From 565ecb637164c8a34a1495bbbe70a8c38926b52e Mon Sep 17 00:00:00 2001 From: Jerry <707344974@qq.com> Date: Fri, 5 Jul 2024 22:42:33 +0800 Subject: [PATCH] =?UTF-8?q?commit=EF=BC=9A=E5=8D=87=E7=BA=A7=E5=88=B0vue3?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=9C=80=E8=BF=91=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E6=8A=80=E6=9C=AF=E6=A0=88=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?sa-token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OrangeFormsOpen-MybatisFlex/.DS_Store | Bin 0 -> 6148 bytes OrangeFormsOpen-MybatisFlex/.gitignore | 26 + OrangeFormsOpen-MybatisFlex/README.md | 21 + .../application-webadmin/pom.xml | 91 + .../webadmin/WebAdminApplication.java | 23 + .../app/util/FlowIdentityExtHelper.java | 244 + .../webadmin/config/ApplicationConfig.java | 38 + .../webadmin/config/DataSourceType.java | 47 + .../webadmin/config/FilterConfig.java | 60 + .../webadmin/config/InterceptorConfig.java | 21 + .../config/MultiDataSourceConfig.java | 77 + .../webadmin/config/ThirdPartyAuthConfig.java | 66 + .../AuthenticationInterceptor.java | 281 + .../webadmin/upms/bo/SysMenuExtraData.java | 55 + .../webadmin/upms/bo/SysMenuPerm.java | 66 + .../upms/controller/GlobalDictController.java | 340 + .../upms/controller/LoginController.java | 475 + .../upms/controller/LoginUserController.java | 89 + .../controller/SysDataPermController.java | 337 + .../upms/controller/SysDeptController.java | 428 + .../upms/controller/SysMenuController.java | 231 + .../controller/SysOperationLogController.java | 63 + .../upms/controller/SysPostController.java | 183 + .../upms/controller/SysRoleController.java | 331 + .../upms/controller/SysUserController.java | 378 + .../upms/dao/SysDataPermDeptMapper.java | 13 + .../webadmin/upms/dao/SysDataPermMapper.java | 43 + .../upms/dao/SysDataPermMenuMapper.java | 13 + .../upms/dao/SysDataPermUserMapper.java | 13 + .../webadmin/upms/dao/SysDeptMapper.java | 33 + .../webadmin/upms/dao/SysDeptPostMapper.java | 33 + .../upms/dao/SysDeptRelationMapper.java | 42 + .../webadmin/upms/dao/SysMenuMapper.java | 40 + .../upms/dao/SysPermWhitelistMapper.java | 13 + .../webadmin/upms/dao/SysPostMapper.java | 52 + .../webadmin/upms/dao/SysRoleMapper.java | 25 + .../webadmin/upms/dao/SysRoleMenuMapper.java | 13 + .../webadmin/upms/dao/SysUserMapper.java | 188 + .../webadmin/upms/dao/SysUserPostMapper.java | 13 + .../webadmin/upms/dao/SysUserRoleMapper.java | 13 + .../upms/dao/mapper/SysDataPermDeptMapper.xml | 8 + .../upms/dao/mapper/SysDataPermMapper.xml | 79 + .../upms/dao/mapper/SysDataPermMenuMapper.xml | 8 + .../upms/dao/mapper/SysDataPermUserMapper.xml | 8 + .../upms/dao/mapper/SysDeptMapper.xml | 70 + .../upms/dao/mapper/SysDeptPostMapper.xml | 46 + .../upms/dao/mapper/SysDeptRelationMapper.xml | 32 + .../upms/dao/mapper/SysMenuMapper.xml | 58 + .../dao/mapper/SysPermWhitelistMapper.xml | 9 + .../upms/dao/mapper/SysPostMapper.xml | 80 + .../upms/dao/mapper/SysRoleMapper.xml | 31 + .../upms/dao/mapper/SysRoleMenuMapper.xml | 8 + .../upms/dao/mapper/SysUserMapper.xml | 294 + .../upms/dao/mapper/SysUserPostMapper.xml | 9 + .../upms/dao/mapper/SysUserRoleMapper.xml | 8 + .../webadmin/upms/dto/SysDataPermDeptDto.java | 27 + .../webadmin/upms/dto/SysDataPermDto.java | 55 + .../webadmin/upms/dto/SysDataPermMenuDto.java | 27 + .../webadmin/upms/dto/SysDeptDto.java | 48 + .../webadmin/upms/dto/SysDeptPostDto.java | 47 + .../webadmin/upms/dto/SysMenuDto.java | 92 + .../webadmin/upms/dto/SysPostDto.java | 47 + .../webadmin/upms/dto/SysRoleDto.java | 32 + .../webadmin/upms/dto/SysUserDto.java | 110 + .../webadmin/upms/model/SysDataPerm.java | 62 + .../webadmin/upms/model/SysDataPermDept.java | 29 + .../webadmin/upms/model/SysDataPermMenu.java | 29 + .../webadmin/upms/model/SysDataPermUser.java | 27 + .../webadmin/upms/model/SysDept.java | 73 + .../webadmin/upms/model/SysDeptPost.java | 39 + .../webadmin/upms/model/SysDeptRelation.java | 31 + .../webadmin/upms/model/SysMenu.java | 96 + .../webadmin/upms/model/SysPermWhitelist.java | 33 + .../webadmin/upms/model/SysPost.java | 48 + .../webadmin/upms/model/SysRole.java | 39 + .../webadmin/upms/model/SysRoleMenu.java | 27 + .../webadmin/upms/model/SysUser.java | 172 + .../webadmin/upms/model/SysUserPost.java | 33 + .../webadmin/upms/model/SysUserRole.java | 27 + .../upms/model/constant/SysMenuType.java | 54 + .../model/constant/SysOnlineMenuPermType.java | 44 + .../upms/model/constant/SysUserStatus.java | 44 + .../upms/model/constant/SysUserType.java | 49 + .../upms/service/SysDataPermService.java | 114 + .../webadmin/upms/service/SysDeptService.java | 170 + .../webadmin/upms/service/SysMenuService.java | 72 + .../upms/service/SysPermWhitelistService.java | 23 + .../webadmin/upms/service/SysPostService.java | 99 + .../webadmin/upms/service/SysRoleService.java | 87 + .../webadmin/upms/service/SysUserService.java | 176 + .../service/impl/SysDataPermServiceImpl.java | 335 + .../upms/service/impl/SysDeptServiceImpl.java | 312 + .../upms/service/impl/SysMenuServiceImpl.java | 233 + .../impl/SysPermWhitelistServiceImpl.java | 47 + .../upms/service/impl/SysPostServiceImpl.java | 177 + .../upms/service/impl/SysRoleServiceImpl.java | 188 + .../upms/service/impl/SysUserServiceImpl.java | 383 + .../webadmin/upms/vo/SysDataPermDeptVo.java | 27 + .../webadmin/upms/vo/SysDataPermMenuVo.java | 27 + .../webadmin/upms/vo/SysDataPermVo.java | 57 + .../webadmin/upms/vo/SysDeptPostVo.java | 39 + .../webadmin/upms/vo/SysDeptVo.java | 65 + .../webadmin/upms/vo/SysMenuVo.java | 90 + .../webadmin/upms/vo/SysPostVo.java | 50 + .../webadmin/upms/vo/SysRoleVo.java | 39 + .../webadmin/upms/vo/SysUserVo.java | 133 + .../src/main/resources/application-dev.yml | 171 + .../src/main/resources/application.yml | 164 + .../src/main/resources/logback-spring.xml | 104 + OrangeFormsOpen-MybatisFlex/common/.DS_Store | Bin 0 -> 10244 bytes .../common/common-core/pom.xml | 115 + .../core/advice/MyControllerAdvice.java | 31 + .../core/advice/MyExceptionHandler.java | 167 + .../core/annotation/DeptFilterColumn.java | 16 + .../core/annotation/DisableDataFilter.java | 17 + .../core/annotation/DisableTenantFilter.java | 28 + .../core/annotation/EnableDataPerm.java | 35 + .../FlowLatestApprovalStatusColumn.java | 16 + .../core/annotation/FlowStatusColumn.java | 16 + .../core/annotation/JobUpdateTimeColumn.java | 16 + .../common/core/annotation/MaskField.java | 50 + .../annotation/MultiDatabaseWriteMethod.java | 18 + .../common/core/annotation/MyDataSource.java | 21 + .../core/annotation/MyDataSourceResolver.java | 35 + .../common/core/annotation/MyRequestBody.java | 26 + .../core/annotation/NoAuthInterface.java | 15 + .../core/annotation/RelationConstDict.java | 29 + .../common/core/annotation/RelationDict.java | 71 + .../core/annotation/RelationGlobalDict.java | 29 + .../core/annotation/RelationManyToMany.java | 39 + .../RelationManyToManyAggregation.java | 96 + .../core/annotation/RelationOneToMany.java | 54 + .../RelationOneToManyAggregation.java | 68 + .../core/annotation/RelationOneToOne.java | 61 + .../core/annotation/TenantFilterColumn.java | 16 + .../core/annotation/UploadFlagColumn.java | 24 + .../core/annotation/UserFilterColumn.java | 16 + .../common/core/aop/DataSourceAspect.java | 48 + .../core/aop/DataSourceResolveAspect.java | 73 + .../common/core/base/dao/BaseDaoMapper.java | 87 + .../core/base/mapper/BaseModelMapper.java | 124 + .../core/base/mapper/DummyModelMapper.java | 58 + .../common/core/base/model/BaseModel.java | 40 + .../core/base/service/BaseDictService.java | 229 + .../common/core/base/service/BaseService.java | 2278 +++ .../core/base/service/IBaseDictService.java | 69 + .../core/base/service/IBaseService.java | 559 + .../common/core/base/vo/BaseVo.java | 35 + .../common/core/cache/CacheConfig.java | 110 + .../common/core/cache/DictionaryCache.java | 89 + .../common/core/cache/MapDictionaryCache.java | 200 + .../core/cache/MapTreeDictionaryCache.java | 138 + .../config/BaseMultiDataSourceConfig.java | 60 + .../core/config/CommonWebMvcConfig.java | 87 + .../common/core/config/CoreProperties.java | 83 + .../core/config/DataSourceContextHolder.java | 52 + .../common/core/config/DataSourceInfo.java | 41 + .../common/core/config/DynamicDataSource.java | 170 + .../common/core/config/EncryptConfig.java | 20 + .../common/core/config/PageHelperConfig.java | 38 + .../core/config/RestTemplateConfig.java | 71 + .../common/core/config/TomcatConfig.java | 39 + .../common/core/constant/AggregationType.java | 81 + .../common/core/constant/AppDeviceType.java | 69 + .../core/constant/ApplicationConstant.java | 161 + .../core/constant/DataPermRuleType.java | 81 + .../common/core/constant/DictType.java | 59 + .../common/core/constant/ErrorCodeEnum.java | 88 + .../common/core/constant/FieldFilterType.java | 127 + .../common/core/constant/FilterParamType.java | 54 + .../core/constant/GlobalDeletedFlag.java | 25 + .../core/constant/MaskFieldTypeEnum.java | 47 + .../common/core/constant/ObjectFieldType.java | 26 + .../common/core/constant/UserFilterGroup.java | 22 + .../exception/DataValidationException.java | 26 + .../exception/InvalidClassFieldException.java | 30 + .../exception/InvalidDataFieldException.java | 30 + .../exception/InvalidDataModelException.java | 27 + .../exception/InvalidDblinkTypeException.java | 24 + .../exception/InvalidRedisModeException.java | 27 + .../exception/MapCacheAccessException.java | 20 + .../core/exception/MyRuntimeException.java | 46 + .../core/exception/NoDataAffectException.java | 26 + .../core/exception/NoDataPermException.java | 26 + .../exception/RedisCacheAccessException.java | 20 + .../MyRequestArgumentResolver.java | 227 + .../listener/LoadServiceRelationListener.java | 28 + .../common/core/object/CallResult.java | 103 + .../common/core/object/ColumnEncodedRule.java | 38 + .../common/core/object/ConstDictInfo.java | 24 + .../common/core/object/DummyClass.java | 27 + .../common/core/object/GlobalThreadLocal.java | 52 + .../common/core/object/LoginUserInfo.java | 62 + .../common/core/object/MyGroupCriteria.java | 24 + .../common/core/object/MyGroupParam.java | 231 + .../common/core/object/MyOrderParam.java | 303 + .../common/core/object/MyPageData.java | 36 + .../common/core/object/MyPageParam.java | 69 + .../common/core/object/MyPrintInfo.java | 32 + .../common/core/object/MyRelationParam.java | 122 + .../common/core/object/MyWhereCriteria.java | 376 + .../common/core/object/ResponseResult.java | 295 + .../common/core/object/TableModelInfo.java | 33 + .../common/core/object/TokenData.java | 134 + .../common/core/object/Tuple2.java | 50 + .../common/core/object/Tuple3.java | 65 + .../common/core/object/TypedCallResult.java | 109 + .../common/core/upload/BaseUpDownloader.java | 216 + .../common/core/upload/LocalUpDownloader.java | 169 + .../core/upload/UpDownloaderFactory.java | 49 + .../core/upload/UploadResponseInfo.java | 33 + .../common/core/upload/UploadStoreInfo.java | 22 + .../core/upload/UploadStoreTypeEnum.java | 31 + .../common/core/util/AopTargetUtil.java | 81 + .../core/util/ApplicationContextHolder.java | 88 + .../common/core/util/ContextUtil.java | 51 + .../common/core/util/DataSourceResolver.java | 21 + .../core/util/DefaultDataSourceResolver.java | 55 + .../common/core/util/ExportUtil.java | 111 + .../common/core/util/ImportUtil.java | 356 + .../orangeforms/common/core/util/IpUtil.java | 104 + .../orangeforms/common/core/util/JwtUtil.java | 112 + .../common/core/util/LogMessageUtil.java | 33 + .../common/core/util/MaskFieldHandler.java | 21 + .../common/core/util/MaskFieldUtil.java | 203 + .../common/core/util/MyCommonUtil.java | 442 + .../core/util/MyCustomMaskFieldHandler.java | 23 + .../common/core/util/MyDateUtil.java | 320 + .../common/core/util/MyModelUtil.java | 875 ++ .../common/core/util/MyPageUtil.java | 155 + .../common/core/util/RedisKeyUtil.java | 187 + .../orangeforms/common/core/util/RsaUtil.java | 102 + .../common/core/util/TreeNode.java | 92 + .../common/core/validator/AddGroup.java | 10 + .../common/core/validator/ConstDictRef.java | 48 + .../core/validator/ConstDictValidator.java | 34 + .../common/core/validator/TextLength.java | 55 + .../core/validator/TextLengthValidator.java | 39 + .../common/core/validator/UpdateGroup.java | 11 + .../common/common-datafilter/pom.xml | 29 + .../aop/DisableDataFilterAspect.java | 42 + .../config/DataFilterAutoConfig.java | 13 + .../config/DataFilterProperties.java | 50 + .../config/DataFilterWebMvcConfigurer.java | 21 + .../interceptor/DataFilterInterceptor.java | 42 + .../MybatisDataFilterInterceptor.java | 646 + .../listener/LoadDataFilterInfoListener.java | 25 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-dbutil/pom.xml | 54 + .../dbutil/constant/CustomDateValueType.java | 83 + .../common/dbutil/constant/DblinkType.java | 74 + .../common/dbutil/object/DatasetFilter.java | 52 + .../common/dbutil/object/DatasetParam.java | 49 + .../dbutil/object/GenericResultSet.java | 39 + .../common/dbutil/object/SqlResultSet.java | 28 + .../common/dbutil/object/SqlTable.java | 41 + .../common/dbutil/object/SqlTableColumn.java | 83 + .../dbutil/provider/DataSourceProvider.java | 108 + .../common/dbutil/provider/JdbcConfig.java | 62 + .../common/dbutil/provider/MySqlConfig.java | 42 + .../common/dbutil/provider/MySqlProvider.java | 112 + .../common/dbutil/util/DataSourceUtil.java | 838 ++ .../common/common-dict/pom.xml | 31 + .../dict/constant/GlobalDictItemStatus.java | 44 + .../common/dict/dao/GlobalDictItemMapper.java | 13 + .../common/dict/dao/GlobalDictMapper.java | 34 + .../dict/dao/TenantGlobalDictItemMapper.java | 54 + .../dict/dao/TenantGlobalDictMapper.java | 13 + .../common/dict/dto/GlobalDictDto.java | 40 + .../common/dict/dto/GlobalDictItemDto.java | 54 + .../common/dict/dto/TenantGlobalDictDto.java | 29 + .../dict/dto/TenantGlobalDictItemDto.java | 18 + .../common/dict/model/GlobalDict.java | 65 + .../common/dict/model/GlobalDictItem.java | 82 + .../common/dict/model/TenantGlobalDict.java | 29 + .../dict/model/TenantGlobalDictItem.java | 23 + .../dict/service/GlobalDictItemService.java | 92 + .../dict/service/GlobalDictService.java | 108 + .../service/TenantGlobalDictItemService.java | 115 + .../dict/service/TenantGlobalDictService.java | 137 + .../impl/GlobalDictItemServiceImpl.java | 143 + .../service/impl/GlobalDictServiceImpl.java | 184 + .../impl/TenantGlobalDictItemServiceImpl.java | 189 + .../impl/TenantGlobalDictServiceImpl.java | 302 + .../dict/util/GlobalDictOperationHelper.java | 89 + .../common/dict/vo/GlobalDictItemVo.java | 77 + .../common/dict/vo/GlobalDictVo.java | 59 + .../dict/vo/TenantGlobalDictItemVo.java | 18 + .../common/dict/vo/TenantGlobalDictVo.java | 29 + .../common/common-ext/pom.xml | 21 + .../common/ext/base/BizWidgetDatasource.java | 41 + .../ext/config/CommonExtAutoConfig.java | 13 + .../ext/config/CommonExtProperties.java | 76 + .../ext/constant/BizWidgetDatasourceType.java | 41 + .../ext/controller/BizWidgetController.java | 58 + .../common/ext/controller/UtilController.java | 112 + .../util/BizWidgetDatasourceExtHelper.java | 209 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-flow-online/pom.xml | 29 + .../online/config/FlowOnlineAutoConfig.java | 13 + .../online/config/FlowOnlineProperties.java | 20 + .../FlowOnlineOperationController.java | 1082 ++ .../service/FlowOnlineOperationService.java | 136 + .../impl/FlowOnlineBusinessServiceImpl.java | 97 + .../impl/FlowOnlineOperationServiceImpl.java | 287 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-flow/pom.xml | 49 + .../flow/advice/FlowExceptionHandler.java | 58 + .../base/service/BaseFlowOnlineService.java | 26 + .../flow/config/CustomEngineConfigurator.java | 48 + .../common/flow/config/FlowAutoConfig.java | 13 + .../common/flow/config/FlowProperties.java | 20 + .../flow/constant/FlowApprovalType.java | 97 + .../common/flow/constant/FlowBackType.java | 25 + .../constant/FlowBuiltinApprovalStatus.java | 33 + .../common/flow/constant/FlowConstant.java | 266 + .../common/flow/constant/FlowTaskStatus.java | 49 + .../common/flow/constant/FlowTaskType.java | 25 + .../controller/FlowCategoryController.java | 232 + .../flow/controller/FlowEntryController.java | 475 + .../FlowEntryVariableController.java | 159 + .../controller/FlowMessageController.java | 110 + .../controller/FlowOperationController.java | 941 ++ .../common/flow/dao/FlowCategoryMapper.java | 26 + .../common/flow/dao/FlowEntryMapper.java | 26 + .../flow/dao/FlowEntryPublishMapper.java | 13 + .../dao/FlowEntryPublishVariableMapper.java | 22 + .../flow/dao/FlowEntryVariableMapper.java | 27 + .../FlowMessageCandidateIdentityMapper.java | 21 + .../FlowMessageIdentityOperationMapper.java | 21 + .../common/flow/dao/FlowMessageMapper.java | 79 + .../dao/FlowMultiInstanceTransMapper.java | 13 + .../flow/dao/FlowTaskCommentMapper.java | 13 + .../common/flow/dao/FlowTaskExtMapper.java | 22 + .../flow/dao/FlowWorkOrderExtMapper.java | 14 + .../common/flow/dao/FlowWorkOrderMapper.java | 49 + .../flow/dao/mapper/FlowCategoryMapper.xml | 56 + .../flow/dao/mapper/FlowEntryMapper.xml | 94 + .../dao/mapper/FlowEntryPublishMapper.xml | 18 + .../mapper/FlowEntryPublishVariableMapper.xml | 30 + .../dao/mapper/FlowEntryVariableMapper.xml | 41 + .../FlowMessageCandidateIdentityMapper.xml | 16 + .../FlowMessageIdentityOperationMapper.xml | 17 + .../flow/dao/mapper/FlowMessageMapper.xml | 112 + .../mapper/FlowMultiInstanceTransMapper.xml | 17 + .../flow/dao/mapper/FlowTaskCommentMapper.xml | 23 + .../flow/dao/mapper/FlowTaskExtMapper.xml | 36 + .../dao/mapper/FlowWorkOrderExtMapper.xml | 15 + .../flow/dao/mapper/FlowWorkOrderMapper.xml | 82 + .../common/flow/dto/FlowCategoryDto.java | 47 + .../common/flow/dto/FlowEntryDto.java | 107 + .../common/flow/dto/FlowEntryVariableDto.java | 81 + .../common/flow/dto/FlowMessageDto.java | 51 + .../common/flow/dto/FlowTaskCommentDto.java | 38 + .../common/flow/dto/FlowWorkOrderDto.java | 39 + .../exception/FlowEmptyUserException.java | 21 + .../exception/FlowOperationException.java | 35 + .../flow/listener/AutoSkipTaskListener.java | 165 + .../flow/listener/DeptPostLeaderListener.java | 27 + .../flow/listener/FlowFinishedListener.java | 56 + .../flow/listener/FlowTaskNotifyListener.java | 80 + .../flow/listener/FlowUserTaskListener.java | 32 + .../listener/UpDeptPostLeaderListener.java | 27 + .../UpdateLatestApprovalStatusListener.java | 44 + .../common/flow/model/FlowCategory.java | 77 + .../common/flow/model/FlowEntry.java | 154 + .../common/flow/model/FlowEntryPublish.java | 89 + .../flow/model/FlowEntryPublishVariable.java | 69 + .../common/flow/model/FlowEntryVariable.java | 77 + .../common/flow/model/FlowMessage.java | 167 + .../model/FlowMessageCandidateIdentity.java | 39 + .../model/FlowMessageIdentityOperation.java | 48 + .../flow/model/FlowMultiInstanceTrans.java | 97 + .../common/flow/model/FlowTaskComment.java | 150 + .../common/flow/model/FlowTaskExt.java | 87 + .../common/flow/model/FlowWorkOrder.java | 162 + .../common/flow/model/FlowWorkOrderExt.java | 71 + .../flow/model/constant/FlowBindFormType.java | 44 + .../flow/model/constant/FlowEntryStatus.java | 44 + .../constant/FlowMessageOperationType.java | 21 + .../flow/model/constant/FlowMessageType.java | 26 + .../flow/model/constant/FlowVariableType.java | 44 + .../flow/object/FlowElementExtProperty.java | 18 + .../flow/object/FlowEntryExtensionData.java | 37 + .../common/flow/object/FlowRumtimeObject.java | 24 + .../flow/object/FlowTaskMultiSignAssign.java | 22 + .../common/flow/object/FlowTaskOperation.java | 38 + .../object/FlowTaskPostCandidateGroup.java | 64 + .../flow/object/FlowUserTaskExtData.java | 63 + .../common/flow/service/FlowApiService.java | 568 + .../flow/service/FlowCategoryService.java | 69 + .../common/flow/service/FlowEntryService.java | 133 + .../service/FlowEntryVariableService.java | 68 + .../flow/service/FlowMessageService.java | 106 + .../FlowMultiInstanceTransService.java | 38 + .../flow/service/FlowTaskCommentService.java | 83 + .../flow/service/FlowTaskExtService.java | 124 + .../flow/service/FlowWorkOrderService.java | 184 + .../flow/service/impl/FlowApiServiceImpl.java | 2032 +++ .../service/impl/FlowCategoryServiceImpl.java | 129 + .../service/impl/FlowEntryServiceImpl.java | 485 + .../impl/FlowEntryVariableServiceImpl.java | 134 + .../service/impl/FlowMessageServiceImpl.java | 384 + .../FlowMultiInstanceTransServiceImpl.java | 87 + .../impl/FlowTaskCommentServiceImpl.java | 140 + .../service/impl/FlowTaskExtServiceImpl.java | 622 + .../impl/FlowWorkOrderServiceImpl.java | 354 + .../flow/util/BaseFlowIdentityExtHelper.java | 253 + .../flow/util/BaseFlowNotifyExtHelper.java | 28 + .../util/BaseOnlineBusinessDataExtHelper.java | 51 + .../CustomChangeActivityStateBuilderImpl.java | 29 + .../util/CustomMoveActivityIdContainer.java | 24 + .../flow/util/FlowCustomExtFactory.java | 67 + .../common/flow/util/FlowOperationHelper.java | 505 + .../common/flow/util/FlowRedisKeyUtil.java | 52 + .../common/flow/vo/FlowCategoryVo.java | 71 + .../common/flow/vo/FlowEntryPublishVo.java | 59 + .../common/flow/vo/FlowEntryVariableVo.java | 77 + .../common/flow/vo/FlowEntryVo.java | 157 + .../common/flow/vo/FlowMessageVo.java | 137 + .../common/flow/vo/FlowTaskCommentVo.java | 113 + .../common/flow/vo/FlowTaskVo.java | 125 + .../common/flow/vo/FlowUserInfoVo.java | 77 + .../common/flow/vo/FlowWorkOrderVo.java | 158 + .../common/flow/vo/TaskInfoVo.java | 85 + ...able.common.engine.impl.EngineConfigurator | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-log/pom.xml | 29 + .../log/annotation/IgnoreResponseLog.java | 16 + .../common/log/annotation/OperationLog.java | 33 + .../common/log/aop/OperationLogAspect.java | 265 + .../log/config/CommonLogAutoConfig.java | 13 + .../log/config/OperationLogProperties.java | 24 + .../common/log/dao/SysOperationLogMapper.java | 34 + .../log/dao/mapper/SysOperationLogMapper.xml | 97 + .../common/log/dto/SysOperationLogDto.java | 77 + .../common/log/model/SysOperationLog.java | 170 + .../model/constant/SysOperationLogType.java | 145 + .../log/service/SysOperationLogService.java | 45 + .../impl/SysOperationLogServiceImpl.java | 84 + .../common/log/vo/SysOperationLogVo.java | 144 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-minio/pom.xml | 29 + .../minio/config/MinioAutoConfiguration.java | 56 + .../common/minio/config/MinioProperties.java | 32 + .../common/minio/util/MinioUpDownloader.java | 115 + .../common/minio/wrapper/MinioTemplate.java | 199 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-online/pom.xml | 64 + .../online/config/OnlineAutoConfig.java | 13 + .../online/config/OnlineProperties.java | 59 + .../controller/OnlineColumnController.java | 517 + .../OnlineDatasourceController.java | 287 + .../OnlineDatasourceRelationController.java | 260 + .../controller/OnlineDblinkController.java | 276 + .../controller/OnlineDictController.java | 221 + .../controller/OnlineFormController.java | 428 + .../controller/OnlineOperationController.java | 1044 ++ .../controller/OnlinePageController.java | 386 + .../controller/OnlineRuleController.java | 175 + .../OnlineVirtualColumnController.java | 195 + .../common/online/dao/OnlineColumnMapper.java | 24 + .../online/dao/OnlineColumnRuleMapper.java | 14 + .../online/dao/OnlineDatasourceMapper.java | 60 + .../dao/OnlineDatasourceRelationMapper.java | 26 + .../dao/OnlineDatasourceTableMapper.java | 13 + .../common/online/dao/OnlineDblinkMapper.java | 26 + .../common/online/dao/OnlineDictMapper.java | 26 + .../dao/OnlineFormDatasourceMapper.java | 13 + .../common/online/dao/OnlineFormMapper.java | 36 + .../online/dao/OnlineOperationMapper.java | 259 + .../dao/OnlinePageDatasourceMapper.java | 13 + .../common/online/dao/OnlinePageMapper.java | 36 + .../common/online/dao/OnlineRuleMapper.java | 52 + .../common/online/dao/OnlineTableMapper.java | 34 + .../online/dao/OnlineVirtualColumnMapper.java | 26 + .../online/dao/mapper/OnlineColumnMapper.xml | 61 + .../dao/mapper/OnlineColumnRuleMapper.xml | 9 + .../dao/mapper/OnlineDatasourceMapper.xml | 93 + .../mapper/OnlineDatasourceRelationMapper.xml | 56 + .../mapper/OnlineDatasourceTableMapper.xml | 10 + .../online/dao/mapper/OnlineDblinkMapper.xml | 47 + .../online/dao/mapper/OnlineDictMapper.xml | 65 + .../dao/mapper/OnlineFormDatasourceMapper.xml | 9 + .../online/dao/mapper/OnlineFormMapper.xml | 79 + .../dao/mapper/OnlinePageDatasourceMapper.xml | 9 + .../online/dao/mapper/OnlinePageMapper.xml | 70 + .../online/dao/mapper/OnlineRuleMapper.xml | 77 + .../online/dao/mapper/OnlineTableMapper.xml | 57 + .../dao/mapper/OnlineVirtualColumnMapper.xml | 56 + .../common/online/dto/OnlineColumnDto.java | 189 + .../online/dto/OnlineColumnRuleDto.java | 38 + .../online/dto/OnlineDatasourceDto.java | 62 + .../dto/OnlineDatasourceRelationDto.java | 107 + .../common/online/dto/OnlineDblinkDto.java | 53 + .../common/online/dto/OnlineDictDto.java | 128 + .../common/online/dto/OnlineFilterDto.java | 72 + .../common/online/dto/OnlineFormDto.java | 91 + .../online/dto/OnlinePageDatasourceDto.java | 39 + .../common/online/dto/OnlinePageDto.java | 58 + .../common/online/dto/OnlineRuleDto.java | 56 + .../common/online/dto/OnlineTableDto.java | 47 + .../online/dto/OnlineVirtualColumnDto.java | 102 + .../exception/OnlineRuntimeException.java | 28 + .../common/online/model/OnlineColumn.java | 215 + .../common/online/model/OnlineColumnRule.java | 36 + .../common/online/model/OnlineDatasource.java | 103 + .../model/OnlineDatasourceRelation.java | 166 + .../online/model/OnlineDatasourceTable.java | 39 + .../common/online/model/OnlineDblink.java | 85 + .../common/online/model/OnlineDict.java | 167 + .../common/online/model/OnlineForm.java | 134 + .../online/model/OnlineFormDatasource.java | 33 + .../common/online/model/OnlinePage.java | 105 + .../online/model/OnlinePageDatasource.java | 33 + .../common/online/model/OnlineRule.java | 98 + .../common/online/model/OnlineTable.java | 99 + .../online/model/OnlineVirtualColumn.java | 87 + .../model/constant/FieldFilterType.java | 79 + .../online/model/constant/FieldKind.java | 109 + .../online/model/constant/FormKind.java | 44 + .../online/model/constant/FormType.java | 64 + .../online/model/constant/PageStatus.java | 49 + .../online/model/constant/PageType.java | 49 + .../online/model/constant/RelationType.java | 44 + .../online/model/constant/RuleType.java | 69 + .../online/model/constant/VirtualType.java | 39 + .../common/online/object/ColumnData.java | 28 + .../common/online/object/ConstDictInfo.java | 24 + .../common/online/object/JoinTableInfo.java | 28 + .../online/service/OnlineColumnService.java | 147 + .../OnlineDatasourceRelationService.java | 85 + .../service/OnlineDatasourceService.java | 134 + .../online/service/OnlineDblinkService.java | 99 + .../online/service/OnlineDictService.java | 78 + .../online/service/OnlineFormService.java | 122 + .../service/OnlineOperationService.java | 220 + .../online/service/OnlinePageService.java | 138 + .../online/service/OnlineRuleService.java | 91 + .../online/service/OnlineTableService.java | 68 + .../service/OnlineVirtualColumnService.java | 68 + .../service/impl/OnlineColumnServiceImpl.java | 357 + .../OnlineDatasourceRelationServiceImpl.java | 285 + .../impl/OnlineDatasourceServiceImpl.java | 266 + .../service/impl/OnlineDblinkServiceImpl.java | 201 + .../service/impl/OnlineDictServiceImpl.java | 187 + .../service/impl/OnlineFormServiceImpl.java | 306 + .../impl/OnlineOperationServiceImpl.java | 1757 +++ .../service/impl/OnlinePageServiceImpl.java | 295 + .../service/impl/OnlineRuleServiceImpl.java | 245 + .../service/impl/OnlineTableServiceImpl.java | 194 + .../impl/OnlineVirtualColumnServiceImpl.java | 176 + .../common/online/util/OnlineConstant.java | 21 + .../online/util/OnlineCustomExtFactory.java | 33 + .../util/OnlineCustomMaskFieldHandler.java | 25 + .../online/util/OnlineDataSourceUtil.java | 47 + .../online/util/OnlineOperationHelper.java | 419 + .../online/util/OnlineRedisKeyUtil.java | 76 + .../common/online/util/OnlineUtil.java | 36 + .../common/online/vo/OnlineColumnRuleVo.java | 33 + .../common/online/vo/OnlineColumnVo.java | 204 + .../online/vo/OnlineDatasourceRelationVo.java | 150 + .../common/online/vo/OnlineDatasourceVo.java | 98 + .../common/online/vo/OnlineDblinkVo.java | 84 + .../common/online/vo/OnlineDictVo.java | 162 + .../common/online/vo/OnlineFormVo.java | 127 + .../online/vo/OnlinePageDatasourceVo.java | 33 + .../common/online/vo/OnlinePageVo.java | 96 + .../common/online/vo/OnlineRuleVo.java | 90 + .../common/online/vo/OnlineTableVo.java | 71 + .../online/vo/OnlineVirtualColumnVo.java | 87 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-redis/pom.xml | 29 + .../redis/cache/RedisDictionaryCache.java | 263 + .../redis/cache/RedisTreeDictionaryCache.java | 224 + .../redis/cache/RedissonCacheConfig.java | 73 + .../redis/cache/SessionCacheHelper.java | 179 + .../common/redis/config/RedissonConfig.java | 105 + .../common/redis/util/CommonRedisUtil.java | 216 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-satoken/pom.xml | 49 + .../satoken/annotation/SaTokenDenyAuth.java | 16 + .../listener/SaTokenPermCodeScanListener.java | 26 + .../common/satoken/util/SaTokenUtil.java | 283 + .../common/satoken/util/StpInterfaceImpl.java | 62 + .../common/common-sequence/pom.xml | 24 + .../config/IdGeneratorAutoConfig.java | 14 + .../config/IdGeneratorProperties.java | 20 + .../sequence/generator/BasicIdGenerator.java | 47 + .../sequence/generator/MyIdGenerator.java | 24 + .../sequence/wrapper/IdGeneratorWrapper.java | 52 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-swagger/pom.xml | 40 + .../config/SwaggerAutoConfiguration.java | 70 + .../swagger/config/SwaggerProperties.java | 45 + .../plugin/MyGlobalOperationCustomer.java | 194 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + OrangeFormsOpen-MybatisFlex/common/pom.xml | 30 + OrangeFormsOpen-MybatisFlex/pom.xml | 176 + .../zz-resource/.DS_Store | Bin 0 -> 6148 bytes .../zz-resource/db-scripts/.DS_Store | Bin 0 -> 6148 bytes .../db-scripts/zzdemo-online-open.sql | 2888 ++++ .../zz-resource/docker-files/.DS_Store | Bin 0 -> 6148 bytes .../docker-files/docker-compose.yml | 33 + .../docker-files/services/redis/Dockerfile | 13 + .../docker-files/services/redis/redis.conf | 1307 ++ OrangeFormsOpen-MybatisPlus/.DS_Store | Bin 0 -> 6148 bytes OrangeFormsOpen-MybatisPlus/.gitignore | 26 + OrangeFormsOpen-MybatisPlus/README.md | 21 + .../application-webadmin/pom.xml | 91 + .../webadmin/WebAdminApplication.java | 23 + .../app/util/FlowIdentityExtHelper.java | 244 + .../webadmin/config/ApplicationConfig.java | 38 + .../webadmin/config/DataSourceType.java | 47 + .../webadmin/config/FilterConfig.java | 60 + .../webadmin/config/InterceptorConfig.java | 21 + .../config/MultiDataSourceConfig.java | 77 + .../webadmin/config/ThirdPartyAuthConfig.java | 66 + .../AuthenticationInterceptor.java | 281 + .../webadmin/upms/bo/SysMenuExtraData.java | 55 + .../webadmin/upms/bo/SysMenuPerm.java | 66 + .../upms/controller/GlobalDictController.java | 340 + .../upms/controller/LoginController.java | 475 + .../upms/controller/LoginUserController.java | 89 + .../controller/SysDataPermController.java | 337 + .../upms/controller/SysDeptController.java | 428 + .../upms/controller/SysMenuController.java | 231 + .../controller/SysOperationLogController.java | 63 + .../upms/controller/SysPostController.java | 183 + .../upms/controller/SysRoleController.java | 331 + .../upms/controller/SysUserController.java | 378 + .../upms/dao/SysDataPermDeptMapper.java | 13 + .../webadmin/upms/dao/SysDataPermMapper.java | 43 + .../upms/dao/SysDataPermMenuMapper.java | 13 + .../upms/dao/SysDataPermUserMapper.java | 13 + .../webadmin/upms/dao/SysDeptMapper.java | 33 + .../webadmin/upms/dao/SysDeptPostMapper.java | 33 + .../upms/dao/SysDeptRelationMapper.java | 42 + .../webadmin/upms/dao/SysMenuMapper.java | 40 + .../upms/dao/SysPermWhitelistMapper.java | 13 + .../webadmin/upms/dao/SysPostMapper.java | 52 + .../webadmin/upms/dao/SysRoleMapper.java | 25 + .../webadmin/upms/dao/SysRoleMenuMapper.java | 13 + .../webadmin/upms/dao/SysUserMapper.java | 188 + .../webadmin/upms/dao/SysUserPostMapper.java | 13 + .../webadmin/upms/dao/SysUserRoleMapper.java | 13 + .../upms/dao/mapper/SysDataPermDeptMapper.xml | 8 + .../upms/dao/mapper/SysDataPermMapper.xml | 79 + .../upms/dao/mapper/SysDataPermMenuMapper.xml | 8 + .../upms/dao/mapper/SysDataPermUserMapper.xml | 8 + .../upms/dao/mapper/SysDeptMapper.xml | 70 + .../upms/dao/mapper/SysDeptPostMapper.xml | 46 + .../upms/dao/mapper/SysDeptRelationMapper.xml | 32 + .../upms/dao/mapper/SysMenuMapper.xml | 58 + .../dao/mapper/SysPermWhitelistMapper.xml | 9 + .../upms/dao/mapper/SysPostMapper.xml | 80 + .../upms/dao/mapper/SysRoleMapper.xml | 31 + .../upms/dao/mapper/SysRoleMenuMapper.xml | 8 + .../upms/dao/mapper/SysUserMapper.xml | 294 + .../upms/dao/mapper/SysUserPostMapper.xml | 9 + .../upms/dao/mapper/SysUserRoleMapper.xml | 8 + .../webadmin/upms/dto/SysDataPermDeptDto.java | 27 + .../webadmin/upms/dto/SysDataPermDto.java | 55 + .../webadmin/upms/dto/SysDataPermMenuDto.java | 27 + .../webadmin/upms/dto/SysDeptDto.java | 48 + .../webadmin/upms/dto/SysDeptPostDto.java | 47 + .../webadmin/upms/dto/SysMenuDto.java | 92 + .../webadmin/upms/dto/SysPostDto.java | 47 + .../webadmin/upms/dto/SysRoleDto.java | 32 + .../webadmin/upms/dto/SysUserDto.java | 110 + .../webadmin/upms/model/SysDataPerm.java | 62 + .../webadmin/upms/model/SysDataPermDept.java | 29 + .../webadmin/upms/model/SysDataPermMenu.java | 29 + .../webadmin/upms/model/SysDataPermUser.java | 27 + .../webadmin/upms/model/SysDept.java | 72 + .../webadmin/upms/model/SysDeptPost.java | 39 + .../webadmin/upms/model/SysDeptRelation.java | 31 + .../webadmin/upms/model/SysMenu.java | 96 + .../webadmin/upms/model/SysPermWhitelist.java | 33 + .../webadmin/upms/model/SysPost.java | 48 + .../webadmin/upms/model/SysRole.java | 39 + .../webadmin/upms/model/SysRoleMenu.java | 27 + .../webadmin/upms/model/SysUser.java | 171 + .../webadmin/upms/model/SysUserPost.java | 33 + .../webadmin/upms/model/SysUserRole.java | 27 + .../upms/model/constant/SysMenuType.java | 54 + .../model/constant/SysOnlineMenuPermType.java | 44 + .../upms/model/constant/SysUserStatus.java | 44 + .../upms/model/constant/SysUserType.java | 49 + .../upms/service/SysDataPermService.java | 114 + .../webadmin/upms/service/SysDeptService.java | 170 + .../webadmin/upms/service/SysMenuService.java | 72 + .../upms/service/SysPermWhitelistService.java | 23 + .../webadmin/upms/service/SysPostService.java | 99 + .../webadmin/upms/service/SysRoleService.java | 87 + .../webadmin/upms/service/SysUserService.java | 176 + .../service/impl/SysDataPermServiceImpl.java | 345 + .../upms/service/impl/SysDeptServiceImpl.java | 316 + .../upms/service/impl/SysMenuServiceImpl.java | 239 + .../impl/SysPermWhitelistServiceImpl.java | 47 + .../upms/service/impl/SysPostServiceImpl.java | 186 + .../upms/service/impl/SysRoleServiceImpl.java | 192 + .../upms/service/impl/SysUserServiceImpl.java | 384 + .../webadmin/upms/vo/SysDataPermDeptVo.java | 27 + .../webadmin/upms/vo/SysDataPermMenuVo.java | 27 + .../webadmin/upms/vo/SysDataPermVo.java | 57 + .../webadmin/upms/vo/SysDeptPostVo.java | 39 + .../webadmin/upms/vo/SysDeptVo.java | 65 + .../webadmin/upms/vo/SysMenuVo.java | 90 + .../webadmin/upms/vo/SysPostVo.java | 50 + .../webadmin/upms/vo/SysRoleVo.java | 39 + .../webadmin/upms/vo/SysUserVo.java | 133 + .../src/main/resources/application-dev.yml | 169 + .../src/main/resources/application.yml | 165 + .../src/main/resources/logback-spring.xml | 104 + OrangeFormsOpen-MybatisPlus/common/.DS_Store | Bin 0 -> 10244 bytes .../common/common-core/pom.xml | 115 + .../core/advice/MyControllerAdvice.java | 31 + .../core/advice/MyExceptionHandler.java | 167 + .../core/annotation/DeptFilterColumn.java | 16 + .../core/annotation/DisableDataFilter.java | 17 + .../core/annotation/DisableTenantFilter.java | 28 + .../core/annotation/EnableDataPerm.java | 35 + .../FlowLatestApprovalStatusColumn.java | 16 + .../core/annotation/FlowStatusColumn.java | 16 + .../core/annotation/JobUpdateTimeColumn.java | 16 + .../common/core/annotation/MaskField.java | 50 + .../annotation/MultiDatabaseWriteMethod.java | 18 + .../common/core/annotation/MyDataSource.java | 21 + .../core/annotation/MyDataSourceResolver.java | 35 + .../common/core/annotation/MyRequestBody.java | 26 + .../core/annotation/NoAuthInterface.java | 15 + .../core/annotation/RelationConstDict.java | 29 + .../common/core/annotation/RelationDict.java | 71 + .../core/annotation/RelationGlobalDict.java | 29 + .../core/annotation/RelationManyToMany.java | 39 + .../RelationManyToManyAggregation.java | 96 + .../core/annotation/RelationOneToMany.java | 54 + .../RelationOneToManyAggregation.java | 68 + .../core/annotation/RelationOneToOne.java | 61 + .../core/annotation/TenantFilterColumn.java | 16 + .../core/annotation/UploadFlagColumn.java | 24 + .../core/annotation/UserFilterColumn.java | 16 + .../common/core/aop/DataSourceAspect.java | 48 + .../core/aop/DataSourceResolveAspect.java | 73 + .../common/core/base/dao/BaseDaoMapper.java | 87 + .../core/base/mapper/BaseModelMapper.java | 124 + .../core/base/mapper/DummyModelMapper.java | 58 + .../common/core/base/model/BaseModel.java | 40 + .../core/base/service/BaseDictService.java | 229 + .../common/core/base/service/BaseService.java | 2368 +++ .../core/base/service/IBaseDictService.java | 69 + .../core/base/service/IBaseService.java | 559 + .../common/core/base/vo/BaseVo.java | 35 + .../common/core/cache/CacheConfig.java | 110 + .../common/core/cache/DictionaryCache.java | 89 + .../common/core/cache/MapDictionaryCache.java | 200 + .../core/cache/MapTreeDictionaryCache.java | 138 + .../config/BaseMultiDataSourceConfig.java | 60 + .../core/config/CommonWebMvcConfig.java | 87 + .../common/core/config/CoreProperties.java | 83 + .../core/config/DataSourceContextHolder.java | 52 + .../common/core/config/DataSourceInfo.java | 41 + .../common/core/config/DynamicDataSource.java | 170 + .../common/core/config/EncryptConfig.java | 20 + .../config/MybatisPlusKeyGeneratorConfig.java | 21 + .../core/config/RestTemplateConfig.java | 71 + .../common/core/config/TomcatConfig.java | 39 + .../common/core/constant/AggregationType.java | 81 + .../common/core/constant/AppDeviceType.java | 69 + .../core/constant/ApplicationConstant.java | 161 + .../core/constant/DataPermRuleType.java | 81 + .../common/core/constant/DictType.java | 59 + .../common/core/constant/ErrorCodeEnum.java | 88 + .../common/core/constant/FieldFilterType.java | 127 + .../common/core/constant/FilterParamType.java | 54 + .../core/constant/GlobalDeletedFlag.java | 25 + .../core/constant/MaskFieldTypeEnum.java | 47 + .../common/core/constant/ObjectFieldType.java | 26 + .../common/core/constant/UserFilterGroup.java | 22 + .../exception/DataValidationException.java | 26 + .../exception/InvalidClassFieldException.java | 30 + .../exception/InvalidDataFieldException.java | 30 + .../exception/InvalidDataModelException.java | 27 + .../exception/InvalidDblinkTypeException.java | 24 + .../exception/InvalidRedisModeException.java | 27 + .../exception/MapCacheAccessException.java | 20 + .../core/exception/MyRuntimeException.java | 46 + .../core/exception/NoDataAffectException.java | 26 + .../core/exception/NoDataPermException.java | 26 + .../exception/RedisCacheAccessException.java | 20 + .../MyRequestArgumentResolver.java | 227 + .../listener/LoadServiceRelationListener.java | 28 + .../common/core/object/CallResult.java | 103 + .../common/core/object/ColumnEncodedRule.java | 38 + .../common/core/object/ConstDictInfo.java | 24 + .../common/core/object/DummyClass.java | 27 + .../common/core/object/GlobalThreadLocal.java | 52 + .../common/core/object/LoginUserInfo.java | 62 + .../common/core/object/MyGroupCriteria.java | 24 + .../common/core/object/MyGroupParam.java | 231 + .../common/core/object/MyOrderParam.java | 303 + .../common/core/object/MyPageData.java | 36 + .../common/core/object/MyPageParam.java | 69 + .../common/core/object/MyPrintInfo.java | 32 + .../common/core/object/MyRelationParam.java | 122 + .../common/core/object/MyWhereCriteria.java | 376 + .../common/core/object/ResponseResult.java | 295 + .../common/core/object/TableModelInfo.java | 33 + .../common/core/object/TokenData.java | 134 + .../common/core/object/Tuple2.java | 50 + .../common/core/object/Tuple3.java | 65 + .../common/core/object/TypedCallResult.java | 109 + .../common/core/upload/BaseUpDownloader.java | 216 + .../common/core/upload/LocalUpDownloader.java | 169 + .../core/upload/UpDownloaderFactory.java | 49 + .../core/upload/UploadResponseInfo.java | 33 + .../common/core/upload/UploadStoreInfo.java | 22 + .../core/upload/UploadStoreTypeEnum.java | 31 + .../common/core/util/AopTargetUtil.java | 81 + .../core/util/ApplicationContextHolder.java | 88 + .../common/core/util/ContextUtil.java | 51 + .../common/core/util/DataSourceResolver.java | 21 + .../core/util/DefaultDataSourceResolver.java | 55 + .../common/core/util/ExportUtil.java | 111 + .../common/core/util/ImportUtil.java | 352 + .../orangeforms/common/core/util/IpUtil.java | 104 + .../orangeforms/common/core/util/JwtUtil.java | 112 + .../common/core/util/LogMessageUtil.java | 33 + .../common/core/util/MaskFieldHandler.java | 21 + .../common/core/util/MaskFieldUtil.java | 203 + .../common/core/util/MyCommonUtil.java | 442 + .../core/util/MyCustomMaskFieldHandler.java | 23 + .../common/core/util/MyDateUtil.java | 320 + .../common/core/util/MyModelUtil.java | 873 ++ .../common/core/util/MyPageUtil.java | 155 + .../common/core/util/RedisKeyUtil.java | 187 + .../orangeforms/common/core/util/RsaUtil.java | 102 + .../common/core/util/TreeNode.java | 92 + .../common/core/validator/AddGroup.java | 10 + .../common/core/validator/ConstDictRef.java | 48 + .../core/validator/ConstDictValidator.java | 34 + .../common/core/validator/TextLength.java | 55 + .../core/validator/TextLengthValidator.java | 39 + .../common/core/validator/UpdateGroup.java | 11 + .../common/common-datafilter/pom.xml | 29 + .../aop/DisableDataFilterAspect.java | 42 + .../config/DataFilterAutoConfig.java | 13 + .../config/DataFilterProperties.java | 50 + .../config/DataFilterWebMvcConfigurer.java | 21 + .../interceptor/DataFilterInterceptor.java | 42 + .../MybatisDataFilterInterceptor.java | 637 + .../listener/LoadDataFilterInfoListener.java | 25 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-dbutil/pom.xml | 54 + .../dbutil/constant/CustomDateValueType.java | 83 + .../common/dbutil/constant/DblinkType.java | 74 + .../common/dbutil/object/DatasetFilter.java | 52 + .../common/dbutil/object/DatasetParam.java | 49 + .../dbutil/object/GenericResultSet.java | 39 + .../common/dbutil/object/SqlResultSet.java | 28 + .../common/dbutil/object/SqlTable.java | 41 + .../common/dbutil/object/SqlTableColumn.java | 83 + .../dbutil/provider/DataSourceProvider.java | 108 + .../common/dbutil/provider/JdbcConfig.java | 62 + .../common/dbutil/provider/MySqlConfig.java | 42 + .../common/dbutil/provider/MySqlProvider.java | 112 + .../common/dbutil/util/DataSourceUtil.java | 840 ++ .../common/common-dict/pom.xml | 31 + .../dict/constant/GlobalDictItemStatus.java | 44 + .../common/dict/dao/GlobalDictItemMapper.java | 13 + .../common/dict/dao/GlobalDictMapper.java | 13 + .../dict/dao/TenantGlobalDictItemMapper.java | 54 + .../dict/dao/TenantGlobalDictMapper.java | 13 + .../common/dict/dto/GlobalDictDto.java | 40 + .../common/dict/dto/GlobalDictItemDto.java | 54 + .../common/dict/dto/TenantGlobalDictDto.java | 29 + .../dict/dto/TenantGlobalDictItemDto.java | 18 + .../common/dict/model/GlobalDict.java | 66 + .../common/dict/model/GlobalDictItem.java | 83 + .../common/dict/model/TenantGlobalDict.java | 29 + .../dict/model/TenantGlobalDictItem.java | 23 + .../dict/service/GlobalDictItemService.java | 92 + .../dict/service/GlobalDictService.java | 108 + .../service/TenantGlobalDictItemService.java | 115 + .../dict/service/TenantGlobalDictService.java | 137 + .../impl/GlobalDictItemServiceImpl.java | 143 + .../service/impl/GlobalDictServiceImpl.java | 190 + .../impl/TenantGlobalDictItemServiceImpl.java | 190 + .../impl/TenantGlobalDictServiceImpl.java | 305 + .../dict/util/GlobalDictOperationHelper.java | 89 + .../common/dict/vo/GlobalDictItemVo.java | 77 + .../common/dict/vo/GlobalDictVo.java | 59 + .../dict/vo/TenantGlobalDictItemVo.java | 18 + .../common/dict/vo/TenantGlobalDictVo.java | 29 + .../common/common-ext/pom.xml | 21 + .../common/ext/base/BizWidgetDatasource.java | 41 + .../ext/config/CommonExtAutoConfig.java | 13 + .../ext/config/CommonExtProperties.java | 76 + .../ext/constant/BizWidgetDatasourceType.java | 41 + .../ext/controller/BizWidgetController.java | 58 + .../common/ext/controller/UtilController.java | 112 + .../util/BizWidgetDatasourceExtHelper.java | 209 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-flow-online/pom.xml | 29 + .../online/config/FlowOnlineAutoConfig.java | 13 + .../online/config/FlowOnlineProperties.java | 20 + .../FlowOnlineOperationController.java | 1089 ++ .../service/FlowOnlineOperationService.java | 136 + .../impl/FlowOnlineBusinessServiceImpl.java | 97 + .../impl/FlowOnlineOperationServiceImpl.java | 287 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-flow/pom.xml | 49 + .../flow/advice/FlowExceptionHandler.java | 58 + .../base/service/BaseFlowOnlineService.java | 26 + .../flow/config/CustomEngineConfigurator.java | 48 + .../common/flow/config/FlowAutoConfig.java | 13 + .../common/flow/config/FlowProperties.java | 20 + .../flow/constant/FlowApprovalType.java | 97 + .../common/flow/constant/FlowBackType.java | 25 + .../constant/FlowBuiltinApprovalStatus.java | 33 + .../common/flow/constant/FlowConstant.java | 266 + .../common/flow/constant/FlowTaskStatus.java | 49 + .../common/flow/constant/FlowTaskType.java | 25 + .../controller/FlowCategoryController.java | 232 + .../flow/controller/FlowEntryController.java | 475 + .../FlowEntryVariableController.java | 159 + .../controller/FlowMessageController.java | 110 + .../controller/FlowOperationController.java | 941 ++ .../common/flow/dao/FlowCategoryMapper.java | 26 + .../common/flow/dao/FlowEntryMapper.java | 26 + .../flow/dao/FlowEntryPublishMapper.java | 13 + .../dao/FlowEntryPublishVariableMapper.java | 22 + .../flow/dao/FlowEntryVariableMapper.java | 27 + .../FlowMessageCandidateIdentityMapper.java | 21 + .../FlowMessageIdentityOperationMapper.java | 21 + .../common/flow/dao/FlowMessageMapper.java | 79 + .../dao/FlowMultiInstanceTransMapper.java | 13 + .../flow/dao/FlowTaskCommentMapper.java | 13 + .../common/flow/dao/FlowTaskExtMapper.java | 22 + .../flow/dao/FlowWorkOrderExtMapper.java | 14 + .../common/flow/dao/FlowWorkOrderMapper.java | 49 + .../flow/dao/mapper/FlowCategoryMapper.xml | 56 + .../flow/dao/mapper/FlowEntryMapper.xml | 94 + .../dao/mapper/FlowEntryPublishMapper.xml | 18 + .../mapper/FlowEntryPublishVariableMapper.xml | 30 + .../dao/mapper/FlowEntryVariableMapper.xml | 41 + .../FlowMessageCandidateIdentityMapper.xml | 16 + .../FlowMessageIdentityOperationMapper.xml | 17 + .../flow/dao/mapper/FlowMessageMapper.xml | 112 + .../mapper/FlowMultiInstanceTransMapper.xml | 17 + .../flow/dao/mapper/FlowTaskCommentMapper.xml | 23 + .../flow/dao/mapper/FlowTaskExtMapper.xml | 36 + .../dao/mapper/FlowWorkOrderExtMapper.xml | 15 + .../flow/dao/mapper/FlowWorkOrderMapper.xml | 82 + .../common/flow/dto/FlowCategoryDto.java | 47 + .../common/flow/dto/FlowEntryDto.java | 107 + .../common/flow/dto/FlowEntryVariableDto.java | 81 + .../common/flow/dto/FlowMessageDto.java | 51 + .../common/flow/dto/FlowTaskCommentDto.java | 38 + .../common/flow/dto/FlowWorkOrderDto.java | 39 + .../exception/FlowEmptyUserException.java | 21 + .../exception/FlowOperationException.java | 35 + .../flow/listener/AutoSkipTaskListener.java | 165 + .../flow/listener/DeptPostLeaderListener.java | 27 + .../flow/listener/FlowFinishedListener.java | 56 + .../flow/listener/FlowTaskNotifyListener.java | 80 + .../flow/listener/FlowUserTaskListener.java | 32 + .../listener/UpDeptPostLeaderListener.java | 27 + .../UpdateLatestApprovalStatusListener.java | 44 + .../common/flow/model/FlowCategory.java | 77 + .../common/flow/model/FlowEntry.java | 154 + .../common/flow/model/FlowEntryPublish.java | 89 + .../flow/model/FlowEntryPublishVariable.java | 69 + .../common/flow/model/FlowEntryVariable.java | 77 + .../common/flow/model/FlowMessage.java | 167 + .../model/FlowMessageCandidateIdentity.java | 39 + .../model/FlowMessageIdentityOperation.java | 48 + .../flow/model/FlowMultiInstanceTrans.java | 97 + .../common/flow/model/FlowTaskComment.java | 150 + .../common/flow/model/FlowTaskExt.java | 87 + .../common/flow/model/FlowWorkOrder.java | 163 + .../common/flow/model/FlowWorkOrderExt.java | 72 + .../flow/model/constant/FlowBindFormType.java | 44 + .../flow/model/constant/FlowEntryStatus.java | 44 + .../constant/FlowMessageOperationType.java | 21 + .../flow/model/constant/FlowMessageType.java | 26 + .../flow/model/constant/FlowVariableType.java | 44 + .../flow/object/FlowElementExtProperty.java | 18 + .../flow/object/FlowEntryExtensionData.java | 37 + .../common/flow/object/FlowRumtimeObject.java | 24 + .../flow/object/FlowTaskMultiSignAssign.java | 22 + .../common/flow/object/FlowTaskOperation.java | 38 + .../object/FlowTaskPostCandidateGroup.java | 64 + .../flow/object/FlowUserTaskExtData.java | 63 + .../common/flow/service/FlowApiService.java | 568 + .../flow/service/FlowCategoryService.java | 69 + .../common/flow/service/FlowEntryService.java | 133 + .../service/FlowEntryVariableService.java | 68 + .../flow/service/FlowMessageService.java | 106 + .../FlowMultiInstanceTransService.java | 38 + .../flow/service/FlowTaskCommentService.java | 84 + .../flow/service/FlowTaskExtService.java | 124 + .../flow/service/FlowWorkOrderService.java | 184 + .../flow/service/impl/FlowApiServiceImpl.java | 2039 +++ .../service/impl/FlowCategoryServiceImpl.java | 131 + .../service/impl/FlowEntryServiceImpl.java | 490 + .../impl/FlowEntryVariableServiceImpl.java | 137 + .../service/impl/FlowMessageServiceImpl.java | 385 + .../FlowMultiInstanceTransServiceImpl.java | 87 + .../impl/FlowTaskCommentServiceImpl.java | 142 + .../service/impl/FlowTaskExtServiceImpl.java | 622 + .../impl/FlowWorkOrderServiceImpl.java | 356 + .../flow/util/BaseFlowIdentityExtHelper.java | 253 + .../flow/util/BaseFlowNotifyExtHelper.java | 28 + .../util/BaseOnlineBusinessDataExtHelper.java | 51 + .../CustomChangeActivityStateBuilderImpl.java | 29 + .../util/CustomMoveActivityIdContainer.java | 24 + .../flow/util/FlowCustomExtFactory.java | 67 + .../common/flow/util/FlowOperationHelper.java | 505 + .../common/flow/util/FlowRedisKeyUtil.java | 52 + .../common/flow/vo/FlowCategoryVo.java | 71 + .../common/flow/vo/FlowEntryPublishVo.java | 59 + .../common/flow/vo/FlowEntryVariableVo.java | 77 + .../common/flow/vo/FlowEntryVo.java | 157 + .../common/flow/vo/FlowMessageVo.java | 137 + .../common/flow/vo/FlowTaskCommentVo.java | 113 + .../common/flow/vo/FlowTaskVo.java | 125 + .../common/flow/vo/FlowUserInfoVo.java | 77 + .../common/flow/vo/FlowWorkOrderVo.java | 158 + .../common/flow/vo/TaskInfoVo.java | 85 + ...able.common.engine.impl.EngineConfigurator | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-log/pom.xml | 29 + .../log/annotation/IgnoreResponseLog.java | 16 + .../common/log/annotation/OperationLog.java | 33 + .../common/log/aop/OperationLogAspect.java | 265 + .../log/config/CommonLogAutoConfig.java | 13 + .../log/config/OperationLogProperties.java | 24 + .../common/log/dao/SysOperationLogMapper.java | 34 + .../log/dao/mapper/SysOperationLogMapper.xml | 97 + .../common/log/dto/SysOperationLogDto.java | 77 + .../common/log/model/SysOperationLog.java | 170 + .../model/constant/SysOperationLogType.java | 145 + .../log/service/SysOperationLogService.java | 45 + .../impl/SysOperationLogServiceImpl.java | 84 + .../common/log/vo/SysOperationLogVo.java | 144 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-minio/pom.xml | 29 + .../minio/config/MinioAutoConfiguration.java | 56 + .../common/minio/config/MinioProperties.java | 32 + .../common/minio/util/MinioUpDownloader.java | 115 + .../common/minio/wrapper/MinioTemplate.java | 199 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-online/pom.xml | 64 + .../online/config/OnlineAutoConfig.java | 13 + .../online/config/OnlineProperties.java | 59 + .../controller/OnlineColumnController.java | 517 + .../OnlineDatasourceController.java | 287 + .../OnlineDatasourceRelationController.java | 260 + .../controller/OnlineDblinkController.java | 276 + .../controller/OnlineDictController.java | 221 + .../controller/OnlineFormController.java | 428 + .../controller/OnlineOperationController.java | 1045 ++ .../controller/OnlinePageController.java | 386 + .../controller/OnlineRuleController.java | 175 + .../OnlineVirtualColumnController.java | 195 + .../common/online/dao/OnlineColumnMapper.java | 24 + .../online/dao/OnlineColumnRuleMapper.java | 14 + .../online/dao/OnlineDatasourceMapper.java | 60 + .../dao/OnlineDatasourceRelationMapper.java | 26 + .../dao/OnlineDatasourceTableMapper.java | 13 + .../common/online/dao/OnlineDblinkMapper.java | 26 + .../common/online/dao/OnlineDictMapper.java | 26 + .../dao/OnlineFormDatasourceMapper.java | 13 + .../common/online/dao/OnlineFormMapper.java | 36 + .../online/dao/OnlineOperationMapper.java | 259 + .../dao/OnlinePageDatasourceMapper.java | 13 + .../common/online/dao/OnlinePageMapper.java | 36 + .../common/online/dao/OnlineRuleMapper.java | 52 + .../common/online/dao/OnlineTableMapper.java | 34 + .../online/dao/OnlineVirtualColumnMapper.java | 26 + .../online/dao/mapper/OnlineColumnMapper.xml | 61 + .../dao/mapper/OnlineColumnRuleMapper.xml | 9 + .../dao/mapper/OnlineDatasourceMapper.xml | 93 + .../mapper/OnlineDatasourceRelationMapper.xml | 56 + .../mapper/OnlineDatasourceTableMapper.xml | 10 + .../online/dao/mapper/OnlineDblinkMapper.xml | 47 + .../online/dao/mapper/OnlineDictMapper.xml | 65 + .../dao/mapper/OnlineFormDatasourceMapper.xml | 9 + .../online/dao/mapper/OnlineFormMapper.xml | 79 + .../dao/mapper/OnlinePageDatasourceMapper.xml | 9 + .../online/dao/mapper/OnlinePageMapper.xml | 70 + .../online/dao/mapper/OnlineRuleMapper.xml | 77 + .../online/dao/mapper/OnlineTableMapper.xml | 57 + .../dao/mapper/OnlineVirtualColumnMapper.xml | 56 + .../common/online/dto/OnlineColumnDto.java | 189 + .../online/dto/OnlineColumnRuleDto.java | 38 + .../online/dto/OnlineDatasourceDto.java | 62 + .../dto/OnlineDatasourceRelationDto.java | 107 + .../common/online/dto/OnlineDblinkDto.java | 53 + .../common/online/dto/OnlineDictDto.java | 128 + .../common/online/dto/OnlineFilterDto.java | 72 + .../common/online/dto/OnlineFormDto.java | 91 + .../online/dto/OnlinePageDatasourceDto.java | 39 + .../common/online/dto/OnlinePageDto.java | 58 + .../common/online/dto/OnlineRuleDto.java | 56 + .../common/online/dto/OnlineTableDto.java | 47 + .../online/dto/OnlineVirtualColumnDto.java | 102 + .../exception/OnlineRuntimeException.java | 28 + .../common/online/model/OnlineColumn.java | 215 + .../common/online/model/OnlineColumnRule.java | 36 + .../common/online/model/OnlineDatasource.java | 103 + .../model/OnlineDatasourceRelation.java | 166 + .../online/model/OnlineDatasourceTable.java | 39 + .../common/online/model/OnlineDblink.java | 85 + .../common/online/model/OnlineDict.java | 167 + .../common/online/model/OnlineForm.java | 132 + .../online/model/OnlineFormDatasource.java | 33 + .../common/online/model/OnlinePage.java | 105 + .../online/model/OnlinePageDatasource.java | 33 + .../common/online/model/OnlineRule.java | 99 + .../common/online/model/OnlineTable.java | 99 + .../online/model/OnlineVirtualColumn.java | 87 + .../model/constant/FieldFilterType.java | 79 + .../online/model/constant/FieldKind.java | 109 + .../online/model/constant/FormKind.java | 44 + .../online/model/constant/FormType.java | 64 + .../online/model/constant/PageStatus.java | 49 + .../online/model/constant/PageType.java | 49 + .../online/model/constant/RelationType.java | 44 + .../online/model/constant/RuleType.java | 69 + .../online/model/constant/VirtualType.java | 39 + .../common/online/object/ColumnData.java | 28 + .../common/online/object/ConstDictInfo.java | 24 + .../common/online/object/JoinTableInfo.java | 28 + .../online/service/OnlineColumnService.java | 147 + .../OnlineDatasourceRelationService.java | 85 + .../service/OnlineDatasourceService.java | 134 + .../online/service/OnlineDblinkService.java | 99 + .../online/service/OnlineDictService.java | 78 + .../online/service/OnlineFormService.java | 122 + .../service/OnlineOperationService.java | 220 + .../online/service/OnlinePageService.java | 138 + .../online/service/OnlineRuleService.java | 91 + .../online/service/OnlineTableService.java | 68 + .../service/OnlineVirtualColumnService.java | 68 + .../service/impl/OnlineColumnServiceImpl.java | 365 + .../OnlineDatasourceRelationServiceImpl.java | 289 + .../impl/OnlineDatasourceServiceImpl.java | 270 + .../service/impl/OnlineDblinkServiceImpl.java | 203 + .../service/impl/OnlineDictServiceImpl.java | 189 + .../service/impl/OnlineFormServiceImpl.java | 313 + .../impl/OnlineOperationServiceImpl.java | 1759 +++ .../service/impl/OnlinePageServiceImpl.java | 299 + .../service/impl/OnlineRuleServiceImpl.java | 248 + .../service/impl/OnlineTableServiceImpl.java | 195 + .../impl/OnlineVirtualColumnServiceImpl.java | 180 + .../common/online/util/OnlineConstant.java | 21 + .../online/util/OnlineCustomExtFactory.java | 33 + .../util/OnlineCustomMaskFieldHandler.java | 25 + .../online/util/OnlineDataSourceUtil.java | 47 + .../online/util/OnlineOperationHelper.java | 419 + .../online/util/OnlineRedisKeyUtil.java | 76 + .../common/online/util/OnlineUtil.java | 36 + .../common/online/vo/OnlineColumnRuleVo.java | 33 + .../common/online/vo/OnlineColumnVo.java | 204 + .../online/vo/OnlineDatasourceRelationVo.java | 150 + .../common/online/vo/OnlineDatasourceVo.java | 97 + .../common/online/vo/OnlineDblinkVo.java | 84 + .../common/online/vo/OnlineDictVo.java | 162 + .../common/online/vo/OnlineFormVo.java | 127 + .../online/vo/OnlinePageDatasourceVo.java | 33 + .../common/online/vo/OnlinePageVo.java | 96 + .../common/online/vo/OnlineRuleVo.java | 90 + .../common/online/vo/OnlineTableVo.java | 71 + .../online/vo/OnlineVirtualColumnVo.java | 87 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-redis/pom.xml | 29 + .../redis/cache/RedisDictionaryCache.java | 263 + .../redis/cache/RedisTreeDictionaryCache.java | 224 + .../redis/cache/RedissonCacheConfig.java | 73 + .../redis/cache/SessionCacheHelper.java | 179 + .../common/redis/config/RedissonConfig.java | 105 + .../common/redis/util/CommonRedisUtil.java | 217 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-satoken/pom.xml | 49 + .../satoken/annotation/SaTokenDenyAuth.java | 16 + .../listener/SaTokenPermCodeScanListener.java | 26 + .../common/satoken/util/SaTokenUtil.java | 283 + .../common/satoken/util/StpInterfaceImpl.java | 62 + .../common/common-sequence/pom.xml | 24 + .../config/IdGeneratorAutoConfig.java | 14 + .../config/IdGeneratorProperties.java | 20 + .../sequence/generator/BasicIdGenerator.java | 47 + .../sequence/generator/MyIdGenerator.java | 24 + .../sequence/wrapper/IdGeneratorWrapper.java | 52 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common/common-swagger/pom.xml | 40 + .../config/SwaggerAutoConfiguration.java | 70 + .../swagger/config/SwaggerProperties.java | 45 + .../plugin/MyGlobalOperationCustomer.java | 194 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + OrangeFormsOpen-MybatisPlus/common/pom.xml | 30 + OrangeFormsOpen-MybatisPlus/pom.xml | 171 + .../zz-resource/.DS_Store | Bin 0 -> 6148 bytes .../zz-resource/db-scripts/.DS_Store | Bin 0 -> 6148 bytes .../db-scripts/zzdemo-online-open.sql | 2888 ++++ .../zz-resource/docker-files/.DS_Store | Bin 0 -> 6148 bytes .../docker-files/docker-compose.yml | 33 + .../docker-files/services/redis/Dockerfile | 13 + .../docker-files/services/redis/redis.conf | 1307 ++ OrangeFormsOpen-VUE3/.editorconfig | 24 + OrangeFormsOpen-VUE3/.env.development | 2 + OrangeFormsOpen-VUE3/.env.production | 2 + OrangeFormsOpen-VUE3/.eslintignore | 7 + .../.eslintrc-auto-import.json | 68 + OrangeFormsOpen-VUE3/.eslintrc.cjs | 67 + OrangeFormsOpen-VUE3/.gitignore | 4 + OrangeFormsOpen-VUE3/.prettierrc.cjs | 20 + OrangeFormsOpen-VUE3/.vscode/settings.json | 10 + OrangeFormsOpen-VUE3/README.md | 25 + OrangeFormsOpen-VUE3/components.d.ts | 141 + OrangeFormsOpen-VUE3/index.html | 13 + OrangeFormsOpen-VUE3/package-lock.json | 12498 ++++++++++++++++ OrangeFormsOpen-VUE3/package.json | 71 + OrangeFormsOpen-VUE3/public/favicon.ico | Bin 0 -> 4286 bytes OrangeFormsOpen-VUE3/src/App.vue | 26 + .../src/api/BaseController.ts | 43 + OrangeFormsOpen-VUE3/src/api/config.ts | 2 + .../src/api/flow/FlowCategoryController.ts | 31 + .../src/api/flow/FlowDictionaryController.ts | 23 + .../src/api/flow/FlowEntryController.ts | 71 + .../api/flow/FlowEntryVariableController.ts | 31 + .../src/api/flow/FlowOperationController.ts | 277 + OrangeFormsOpen-VUE3/src/api/flow/index.ts | 11 + .../src/api/online/OnlineColumnController.ts | 84 + .../api/online/OnlineDatasourceController.ts | 35 + .../OnlineDatasourceRelationController.ts | 39 + .../src/api/online/OnlineDblinkController.ts | 53 + .../src/api/online/OnlineDictController.ts | 40 + .../src/api/online/OnlineFormController.ts | 43 + .../api/online/OnlineOperationController.ts | 91 + .../src/api/online/OnlinePageController.ts | 100 + .../src/api/online/OnlineRuleController.ts | 35 + .../online/OnlineVirtualColumnController.ts | 35 + OrangeFormsOpen-VUE3/src/api/online/index.ts | 23 + .../src/api/system/DictionaryController.ts | 217 + .../src/api/system/LoginController.ts | 14 + .../src/api/system/LoginUserController.ts | 21 + .../src/api/system/MenuController.ts | 47 + .../src/api/system/MobileEntryController.ts | 42 + .../src/api/system/OperationLogController.ts | 15 + .../src/api/system/PermCodeController.ts | 13 + .../src/api/system/PermController.ts | 86 + .../src/api/system/SysCommonBizController.ts | 18 + .../src/api/system/SysDataPermController.ts | 80 + .../src/api/system/SysDeptController.ts | 63 + .../src/api/system/SysGlobalDictController.ts | 53 + .../src/api/system/SysPostController.ts | 27 + .../src/api/system/SystemRoleController.ts | 61 + .../src/api/system/UserController.ts | 62 + OrangeFormsOpen-VUE3/src/api/system/index.ts | 31 + OrangeFormsOpen-VUE3/src/assets/img/add.png | Bin 0 -> 237 bytes .../src/assets/img/advance-add-active.png | Bin 0 -> 209 bytes .../src/assets/img/advance-add.png | Bin 0 -> 217 bytes .../src/assets/img/advance-del-active.png | Bin 0 -> 279 bytes .../src/assets/img/advance-del.png | Bin 0 -> 308 bytes .../src/assets/img/advance-edit-active.png | Bin 0 -> 292 bytes .../src/assets/img/advance-edit.png | Bin 0 -> 314 bytes OrangeFormsOpen-VUE3/src/assets/img/back.png | Bin 0 -> 347 bytes OrangeFormsOpen-VUE3/src/assets/img/back2.png | Bin 0 -> 309 bytes .../src/assets/img/collapse.png | Bin 0 -> 264 bytes .../src/assets/img/datasource-active.png | Bin 0 -> 539 bytes .../src/assets/img/datasource.png | Bin 0 -> 640 bytes .../src/assets/img/default-header.jpg | Bin 0 -> 14882 bytes .../src/assets/img/default.jpg | Bin 0 -> 20200 bytes .../src/assets/img/demo-h5-qrcode.png | Bin 0 -> 39426 bytes .../src/assets/img/density.png | Bin 0 -> 455 bytes .../src/assets/img/document-active.png | Bin 0 -> 643 bytes .../src/assets/img/document.png | Bin 0 -> 751 bytes OrangeFormsOpen-VUE3/src/assets/img/down.png | Bin 0 -> 477 bytes OrangeFormsOpen-VUE3/src/assets/img/empty.png | Bin 0 -> 8205 bytes .../src/assets/img/eye_close.png | Bin 0 -> 771 bytes .../src/assets/img/eye_open.png | Bin 0 -> 1125 bytes .../src/assets/img/filter.png | Bin 0 -> 479 bytes .../src/assets/img/import.png | Bin 0 -> 531 bytes OrangeFormsOpen-VUE3/src/assets/img/login.png | Bin 0 -> 728962 bytes .../src/assets/img/login_bg.jpg | Bin 0 -> 494030 bytes .../src/assets/img/login_bg.png | Bin 0 -> 453266 bytes .../src/assets/img/login_bg2.png | Bin 0 -> 1278569 bytes .../src/assets/img/login_icon.png | Bin 0 -> 188819 bytes .../src/assets/img/login_icon2.png | Bin 0 -> 304438 bytes .../src/assets/img/login_logo.png | Bin 0 -> 2342 bytes .../src/assets/img/login_logo2.png | Bin 0 -> 7499 bytes .../src/assets/img/login_password.png | Bin 0 -> 531 bytes .../src/assets/img/login_title.png | Bin 0 -> 11490 bytes .../src/assets/img/login_username.png | Bin 0 -> 665 bytes OrangeFormsOpen-VUE3/src/assets/img/logo.jpg | Bin 0 -> 19013 bytes OrangeFormsOpen-VUE3/src/assets/img/logo.png | Bin 0 -> 9808 bytes .../src/assets/img/logo_white.png | Bin 0 -> 1472 bytes OrangeFormsOpen-VUE3/src/assets/img/more.png | Bin 0 -> 415 bytes .../src/assets/img/orange-group1.png | Bin 0 -> 10894 bytes .../src/assets/img/orange-group2.png | Bin 0 -> 4705 bytes .../src/assets/img/orange-group3.png | Bin 0 -> 4691 bytes .../src/assets/img/orange-group4.png | Bin 0 -> 9959 bytes .../src/assets/img/orange.png | Bin 0 -> 4548 bytes .../src/assets/img/preview.png | Bin 0 -> 880 bytes .../src/assets/img/reduce.png | Bin 0 -> 402 bytes .../src/assets/img/refresh.png | Bin 0 -> 559 bytes .../src/assets/img/refresh2.png | Bin 0 -> 398 bytes .../src/assets/img/remind.png | Bin 0 -> 558 bytes .../src/assets/img/resume_icon_add.png | Bin 0 -> 364 bytes .../src/assets/img/right-icon.png | Bin 0 -> 35798 bytes .../src/assets/img/s-home.png | Bin 0 -> 334 bytes .../src/assets/img/setting.png | Bin 0 -> 876 bytes OrangeFormsOpen-VUE3/src/assets/img/sp1.png | Bin 0 -> 309 bytes OrangeFormsOpen-VUE3/src/assets/img/spjd.png | Bin 0 -> 753 bytes OrangeFormsOpen-VUE3/src/assets/img/spjd2.png | Bin 0 -> 542 bytes OrangeFormsOpen-VUE3/src/assets/img/tj.png | Bin 0 -> 540 bytes OrangeFormsOpen-VUE3/src/assets/img/tj2.png | Bin 0 -> 396 bytes OrangeFormsOpen-VUE3/src/assets/img/vant.png | Bin 0 -> 69211 bytes OrangeFormsOpen-VUE3/src/assets/img/wg.png | Bin 0 -> 495 bytes OrangeFormsOpen-VUE3/src/assets/img/wg2.png | Bin 0 -> 310 bytes .../src/assets/online-icon/iconfont.css | 331 + .../src/assets/online-icon/iconfont.ttf | Bin 0 -> 37812 bytes .../src/assets/online-icon/iconfont.woff | Bin 0 -> 19384 bytes .../src/assets/online-icon/iconfont.woff2 | Bin 0 -> 15964 bytes .../src/assets/skin/orange/index.scss | 87 + .../src/assets/style/base.scss | 1110 ++ .../src/assets/style/chart.scss | 60 + .../src/assets/style/form-style.scss | 104 + .../src/assets/style/index.scss | 4 + .../src/assets/style/transition.scss | 30 + OrangeFormsOpen-VUE3/src/assets/vue.svg | 1 + .../src/common/hooks/useCommon.ts | 73 + .../src/common/hooks/useDate.ts | 92 + .../src/common/hooks/useDownload.ts | 60 + .../src/common/hooks/useDropdown.ts | 85 + .../src/common/hooks/usePermission.ts | 25 + .../src/common/hooks/useTable.ts | 197 + .../src/common/hooks/useUpload.ts | 160 + .../src/common/hooks/useUploadWidget.ts | 25 + .../src/common/hooks/useUrl.ts | 40 + .../src/common/hooks/useWindowResize.ts | 50 + OrangeFormsOpen-VUE3/src/common/http/axios.ts | 147 + .../src/common/http/config.ts | 4 + .../src/common/http/request.ts | 384 + .../src/common/http/types.d.ts | 39 + .../src/common/staticDict/combined.ts | 5 + .../src/common/staticDict/flow.ts | 331 + .../src/common/staticDict/index.ts | 946 ++ .../src/common/staticDict/online.ts | 411 + .../src/common/staticDict/types.ts | 78 + .../src/common/types/list.d.ts | 21 + .../src/common/types/pagination.d.ts | 53 + .../src/common/types/sortinfo.d.ts | 5 + .../src/common/types/table.d.ts | 11 + .../src/common/utils/index.ts | 664 + .../src/common/utils/validate.ts | 30 + .../src/components/AdvanceQuery/index.vue | 240 + .../src/components/Btns/RightAddBtn.vue | 44 + .../src/components/DateRange/index.vue | 271 + .../components/DeptSelect/DeptSelectDlg.vue | 240 + .../src/components/DeptSelect/index.vue | 211 + .../src/components/Dialog/index.ts | 164 + .../src/components/Dialog/layout.vue | 7 + .../src/components/Dialog/types.d.ts | 5 + .../src/components/Dialog/useDialog.ts | 28 + .../src/components/FilterBox/index.vue | 77 + .../src/components/IconSelect/icon.json | 280 + .../src/components/IconSelect/index.vue | 126 + .../src/components/InputNumberRange/index.vue | 236 + .../src/components/MultiItemBox/index.vue | 175 + .../src/components/MultiItemList/index.vue | 213 + .../src/components/PageCloseButton/index.vue | 19 + .../src/components/Progress/index.vue | 36 + .../src/components/RichEditor/index.vue | 171 + .../SpreadSheet/algorithm/bitmap.js | 11 + .../SpreadSheet/algorithm/expression.js | 39 + .../assets/material_common_sprite82.svg | 742 + .../components/SpreadSheet/assets/sprite.svg | 137 + .../src/components/SpreadSheet/canvas/draw.js | 507 + .../components/SpreadSheet/canvas/draw2.js | 90 + .../SpreadSheet/component/border_palette.js | 62 + .../SpreadSheet/component/bottombar.js | 204 + .../SpreadSheet/component/button.js | 11 + .../SpreadSheet/component/calendar.js | 112 + .../SpreadSheet/component/color_palette.js | 124 + .../SpreadSheet/component/contextmenu.js | 99 + .../SpreadSheet/component/datepicker.js | 39 + .../SpreadSheet/component/dropdown.js | 70 + .../SpreadSheet/component/dropdown_align.js | 26 + .../SpreadSheet/component/dropdown_border.js | 15 + .../SpreadSheet/component/dropdown_color.js | 22 + .../SpreadSheet/component/dropdown_font.js | 18 + .../component/dropdown_fontsize.js | 18 + .../SpreadSheet/component/dropdown_format.js | 35 + .../SpreadSheet/component/dropdown_formula.js | 19 + .../component/dropdown_linetype.js | 48 + .../SpreadSheet/component/editor.js | 284 + .../SpreadSheet/component/element.js | 268 + .../components/SpreadSheet/component/event.js | 148 + .../SpreadSheet/component/form_field.js | 66 + .../SpreadSheet/component/form_input.js | 30 + .../SpreadSheet/component/form_select.js | 53 + .../components/SpreadSheet/component/icon.js | 14 + .../SpreadSheet/component/message.js | 31 + .../components/SpreadSheet/component/modal.js | 45 + .../SpreadSheet/component/modal_validation.js | 214 + .../components/SpreadSheet/component/print.js | 213 + .../SpreadSheet/component/resizer.js | 118 + .../SpreadSheet/component/scrollbar.js | 47 + .../SpreadSheet/component/selector.js | 401 + .../components/SpreadSheet/component/sheet.js | 1089 ++ .../SpreadSheet/component/sort_filter.js | 148 + .../SpreadSheet/component/suggest.js | 138 + .../components/SpreadSheet/component/table.js | 395 + .../SpreadSheet/component/toolbar.js | 258 + .../SpreadSheet/component/toolbar/align.js | 13 + .../component/toolbar/autofilter.js | 11 + .../SpreadSheet/component/toolbar/bold.js | 7 + .../SpreadSheet/component/toolbar/border.js | 12 + .../component/toolbar/clearformat.js | 7 + .../component/toolbar/dropdown_item.js | 25 + .../component/toolbar/fill_color.js | 13 + .../SpreadSheet/component/toolbar/font.js | 16 + .../component/toolbar/font_size.js | 16 + .../SpreadSheet/component/toolbar/format.js | 16 + .../SpreadSheet/component/toolbar/formula.js | 16 + .../SpreadSheet/component/toolbar/freeze.js | 7 + .../component/toolbar/icon_item.js | 15 + .../SpreadSheet/component/toolbar/index.js | 241 + .../SpreadSheet/component/toolbar/italic.js | 7 + .../SpreadSheet/component/toolbar/item.js | 35 + .../SpreadSheet/component/toolbar/merge.js | 11 + .../SpreadSheet/component/toolbar/more.js | 35 + .../component/toolbar/paintformat.js | 11 + .../SpreadSheet/component/toolbar/print.js | 7 + .../SpreadSheet/component/toolbar/redo.js | 7 + .../SpreadSheet/component/toolbar/strike.js | 7 + .../component/toolbar/text_color.js | 13 + .../SpreadSheet/component/toolbar/textwrap.js | 7 + .../component/toolbar/toggle_item.js | 28 + .../component/toolbar/underline.js | 7 + .../SpreadSheet/component/toolbar/undo.js | 7 + .../SpreadSheet/component/toolbar/valign.js | 13 + .../SpreadSheet/component/tooltip.js | 27 + .../src/components/SpreadSheet/config.js | 6 + .../SpreadSheet/core/_.prototypes.js | 28 + .../components/SpreadSheet/core/alphabet.js | 117 + .../SpreadSheet/core/auto_filter.js | 183 + .../src/components/SpreadSheet/core/cell.js | 224 + .../components/SpreadSheet/core/cell_range.js | 214 + .../components/SpreadSheet/core/clipboard.js | 35 + .../src/components/SpreadSheet/core/col.js | 80 + .../components/SpreadSheet/core/data_proxy.js | 1252 ++ .../src/components/SpreadSheet/core/font.js | 71 + .../src/components/SpreadSheet/core/format.js | 106 + .../components/SpreadSheet/core/formula.js | 93 + .../src/components/SpreadSheet/core/helper.js | 147 + .../components/SpreadSheet/core/history.js | 37 + .../src/components/SpreadSheet/core/merge.js | 104 + .../src/components/SpreadSheet/core/row.js | 371 + .../src/components/SpreadSheet/core/scroll.js | 8 + .../components/SpreadSheet/core/selector.js | 22 + .../components/SpreadSheet/core/validation.js | 134 + .../components/SpreadSheet/core/validator.js | 111 + .../src/components/SpreadSheet/index.d.ts | 193 + .../src/components/SpreadSheet/index.js | 143 + .../src/components/SpreadSheet/index.scss | 1234 ++ .../src/components/SpreadSheet/locale/de.js | 57 + .../src/components/SpreadSheet/locale/en.js | 149 + .../components/SpreadSheet/locale/locale.js | 75 + .../src/components/SpreadSheet/locale/nl.js | 57 + .../components/SpreadSheet/locale/zh-cn.js | 149 + .../src/components/StepBar/index.vue | 43 + .../src/components/StepBar/stepItem.vue | 60 + .../src/components/TableBox/index.vue | 294 + .../components/TableProgressColumn/index.vue | 101 + .../components/UserSelect/UserSelectDlg.vue | 251 + .../src/components/UserSelect/index.vue | 217 + .../src/components/icons/index.vue | 46 + .../layout/components/BreadCrumb.vue | 66 + .../components/layout/components/Sidebar.vue | 141 + .../components/layout/components/SubMenu.vue | 72 + .../components/layout/components/TagItem.vue | 117 + .../components/layout/components/TagPanel.vue | 282 + .../src/components/layout/components/hooks.ts | 51 + .../layout/components/multi-column-menu.vue | 153 + .../layout/components/multi-column.vue | 143 + .../src/components/layout/index.vue | 495 + .../src/components/thirdParty/hooks.ts | 114 + .../src/components/thirdParty/index.vue | 47 + .../src/components/thirdParty/types.ts | 4 + OrangeFormsOpen-VUE3/src/index.scss | 408 + OrangeFormsOpen-VUE3/src/main.ts | 52 + .../online/components/ActiveWidgetMenu.vue | 56 + .../src/online/components/OnlineBaseCard.vue | 262 + .../src/online/components/OnlineCardTable.vue | 217 + .../online/components/OnlineCustomBlock.vue | 325 + .../online/components/OnlineCustomImage.vue | 142 + .../online/components/OnlineCustomLabel.vue | 88 + .../online/components/OnlineCustomTable.vue | 567 + .../online/components/OnlineCustomTabs.vue | 95 + .../online/components/OnlineCustomText.vue | 69 + .../online/components/OnlineCustomTree.vue | 167 + .../online/components/OnlineCustomUpload.vue | 278 + .../online/components/OnlineCustomWidget.vue | 579 + .../components/OnlineCustomWorkFlowTable.vue | 496 + .../AttributeCollapse/editWidgetAttribute.vue | 264 + .../AttributeCollapse/index.vue | 91 + .../AttributeForm/editWidgetAttribute.vue | 270 + .../AttributeForm/index.vue | 86 + .../editDictParamValue.vue | 188 + .../CustomWidgetDictSetting/index.vue | 223 + .../DateViewTablePagerSetting/index.vue | 54 + .../editCustomListOrder.vue | 179 + .../OnlineCustomListOrderSetting/index.vue | 98 + .../components/OnlineImageUrlInput.vue | 68 + .../editNumberRangeQuick.vue | 98 + .../index.vue | 84 + .../editOnlineTabPanel.vue | 120 + .../OnlineTabPanelSetting/index.vue | 97 + .../editOnlineTableColumn.vue | 227 + .../OnlineTableColumnSetting/index.vue | 101 + .../src/online/components/hooks/widget.ts | 52 + .../src/online/components/types/widget.ts | 11 + .../src/online/config/baseCard.ts | 54 + .../src/online/config/cascader.ts | 80 + .../src/online/config/checkbox.ts | 75 + .../src/online/config/customBlock.ts | 28 + .../src/online/config/date.ts | 100 + .../src/online/config/dateRange.ts | 97 + .../src/online/config/deptSelect.ts | 72 + .../src/online/config/image.ts | 118 + .../src/online/config/index.ts | 282 + .../src/online/config/input.ts | 129 + .../src/online/config/label.ts | 30 + .../src/online/config/link.ts | 105 + .../src/online/config/numberInput.ts | 122 + .../src/online/config/numberRange.ts | 77 + .../src/online/config/radio.ts | 88 + .../src/online/config/richEditor.ts | 45 + .../src/online/config/select.ts | 80 + .../src/online/config/switch.ts | 77 + .../src/online/config/table.ts | 155 + .../src/online/config/tabs.ts | 51 + .../src/online/config/text.ts | 132 + .../src/online/config/tree.ts | 83 + .../src/online/config/upload.ts | 134 + .../src/online/config/userSelect.ts | 72 + .../src/online/config/workOrderList.ts | 32 + OrangeFormsOpen-VUE3/src/pages/error/404.vue | 5 + .../src/pages/login/index.vue | 273 + .../OnlineAdvanceQueryForm/index.vue | 868 ++ .../OnlinePageRender/OnlineEditForm/index.vue | 616 + .../OnlineOneToOneForm/OnlineFilterBox.vue | 355 + .../OnlineOneToOneForm/index.vue | 558 + .../OnlineQueryForm/OnlineFilterBox.vue | 347 + .../OnlineQueryForm/index.vue | 648 + .../OnlineWorkFlowForm/index.vue | 357 + .../OnlineWorkOrderForm/index.vue | 472 + .../online/OnlinePageRender/hooks/useForm.ts | 925 ++ .../OnlinePageRender/hooks/useFormExpose.ts | 19 + .../pages/online/OnlinePageRender/index.vue | 142 + .../online/editOnlinePage/basic/index.vue | 128 + .../dataModel/editOnlinePageDatasource.vue | 257 + .../editOnlinePageDatasourceRelation.vue | 492 + .../dataModel/editVirtualColumnFilter.vue | 286 + .../online/editOnlinePage/dataModel/indev.vue | 499 + .../dataModel/onlinePageTableColumnRule.vue | 834 ++ .../dataModel/onlinePageVirtualColumn.vue | 758 + .../dataModel/setOnlineTableColumnRule.vue | 378 + .../online/editOnlinePage/editOnlineForm.vue | 280 + .../editOnlinePageDatasource.vue | 253 + .../editOnlinePageDatasourceRelation.vue | 478 + .../editVirtualColumnFilter.vue | 277 + .../components/CustomFormOperateSetting.vue | 186 + .../components/CustomFormSetting.vue | 303 + .../CustomTableContainerSetting.vue | 187 + .../CustomWidgetAttributeSetting.vue | 116 + .../components/CustomWidgetBindData.vue | 346 + .../EditDictParamValue.vue | 182 + .../CustomWidgetDictSetting/index.vue | 202 + .../index.vue | 156 + .../components/EditCustomFormOperate.vue | 485 + .../formDesign/components/EditFormField.vue | 97 + .../components/EditWidgetAttribute.vue | 219 + .../editOnlineTabPanel.vue | 117 + .../OnlineTabPanelSetting/index.vue | 104 + .../editOnlineTableColumn.vue | 233 + .../OnlineTableColumnSetting/index.vue | 109 + .../formDesign/editTableColumn.vue | 225 + .../editOnlinePage/formDesign/index.vue | 1352 ++ .../src/pages/online/editOnlinePage/index.vue | 752 + .../setOnlineTableColumnRule.vue | 375 + .../formOnlineDblink/EditOnlineDblink.vue | 424 + .../pages/online/formOnlineDblink/index.vue | 209 + .../formOnlineDict/EditDictDataButton.vue | 96 + .../online/formOnlineDict/EditOnlineDict.vue | 845 ++ .../src/pages/online/formOnlineDict/index.vue | 306 + .../src/pages/online/formOnlinePage/index.vue | 312 + .../src/pages/online/hooks/useDict.ts | 127 + .../src/pages/online/hooks/useFormConfig.ts | 343 + .../pages/online/hooks/useWidgetToolkit.ts | 152 + .../src/pages/upms/formEditDictData/index.vue | 160 + .../pages/upms/formEditGlobalDict/index.vue | 92 + .../pages/upms/formEditSysDataPerm/index.vue | 339 + .../src/pages/upms/formEditSysDept/index.vue | 262 + .../pages/upms/formEditSysMenu/editColumn.vue | 124 + .../src/pages/upms/formEditSysMenu/index.vue | 562 + .../src/pages/upms/formEditSysPerm/index.vue | 195 + .../pages/upms/formEditSysPermCode/index.vue | 292 + .../upms/formEditSysPermModule/index.vue | 271 + .../src/pages/upms/formEditSysPost/index.vue | 153 + .../src/pages/upms/formEditSysRole/index.vue | 217 + .../src/pages/upms/formEditSysUser/index.vue | 358 + .../formSysDataPerm/TabContentDataPerm.vue | 322 + .../TabContentDataPermUser.vue | 305 + .../formSetSysDataPermUser.vue | 252 + .../src/pages/upms/formSysDataPerm/index.vue | 129 + .../src/pages/upms/formSysDept/index.vue | 268 + .../upms/formSysDeptPost/formSetDeptPost.vue | 257 + .../src/pages/upms/formSysDeptPost/index.vue | 332 + .../src/pages/upms/formSysDict/index.vue | 646 + .../src/pages/upms/formSysLoginUser/index.vue | 183 + .../upms/formSysMenu/formSysColumnMenu.vue | 407 + .../src/pages/upms/formSysMenu/hooks.ts | 26 + .../src/pages/upms/formSysMenu/index.vue | 265 + .../SysOperationLogDetail.vue | 219 + .../pages/upms/formSysOperationLog/index.vue | 324 + .../pages/upms/formSysPerm/PermGroupList.vue | 241 + .../src/pages/upms/formSysPerm/PermList.vue | 296 + .../pages/upms/formSysPerm/SysPermDetail.vue | 452 + .../src/pages/upms/formSysPerm/index.vue | 125 + .../formSysPermCode/formSysPermCodeDetail.vue | 344 + .../src/pages/upms/formSysPermCode/index.vue | 450 + .../src/pages/upms/formSysPost/index.vue | 278 + .../pages/upms/formSysRole/TabContentRole.vue | 276 + .../pages/upms/formSysRole/TabContentUser.vue | 311 + .../upms/formSysRole/formSetRoleUser.vue | 252 + .../src/pages/upms/formSysRole/index.vue | 130 + .../src/pages/upms/formSysUser/index.vue | 389 + .../src/pages/welcome/index.vue | 552 + .../src/pages/welcome/index_bak.vue | 208 + .../CopyForSelect/addCopyForItem.vue | 352 + .../CopyForSelect/copyForSetting.vue | 435 + .../components/CopyForSelect/index.vue | 438 + .../workflow/components/HandlerFlowTask.vue | 635 + .../workflow/components/ProcessDesigner.vue | 277 + .../workflow/components/ProcessViewer.vue | 634 + .../pages/workflow/components/TagSelect.vue | 86 + .../pages/workflow/components/TaskCommit.vue | 380 + .../workflow/components/TaskGroupSelect.vue | 98 + .../components/TaskMultipleSelect.vue | 103 + .../workflow/components/TaskPostSelect.vue | 171 + .../workflow/components/TaskUserSelect.vue | 323 + .../components/UserTaskSelect/index.vue | 143 + .../UserTaskSelect/userTaskSelectDlg.vue | 458 + .../flowCategory/formEditFlowCategory.vue | 261 + .../flowCategory/formFlowCategory.vue | 292 + .../workflow/flowEntry/formEditFlowEntry.vue | 1250 ++ .../flowEntry/formEditFlowEntryStatus.vue | 153 + .../flowEntry/formEditFlowEntryVariable.vue | 228 + .../workflow/flowEntry/formFlowEntry.vue | 541 + .../flowEntry/formPublishedFlowEntry.vue | 258 + .../src/pages/workflow/formMessage/index.vue | 346 + .../pages/workflow/handlerFlowTask/hook.ts | 156 + .../pages/workflow/handlerFlowTask/index.vue | 534 + .../pages/workflow/handlerFlowTask/types.ts | 37 + .../process-designer/ProcessDesigner.vue | 835 ++ .../package/process-designer/highlight.ts | 81 + .../plugins/content-pad/contentPadProvider.js | 423 + .../plugins/content-pad/index.js | 6 + .../process-designer/plugins/defaultEmpty.ts | 77 + .../descriptor/activitiDescriptor.json | 1218 ++ .../plugins/descriptor/camundaDescriptor.json | 1010 ++ .../descriptor/flowableDescriptor.json | 1265 ++ .../activiti/activitiExtension.js | 79 + .../extension-moddle/activiti/index.js | 9 + .../extension-moddle/camunda/extension.js | 148 + .../plugins/extension-moddle/camunda/index.js | 6 + .../flowable/flowableExtension.ts | 78 + .../extension-moddle/flowable/index.ts | 9 + .../plugins/translate/customTranslate.ts | 46 + .../process-designer/plugins/translate/zh.ts | 244 + .../package/refactor/PropertiesPanel.vue | 345 + .../package/refactor/autoAgree/index.vue | 91 + .../package/refactor/base/ElementBaseInfo.vue | 79 + .../package/refactor/copy-for/index.vue | 116 + .../refactor/flow-condition/FlowCondition.vue | 288 + .../package/refactor/form-variable/index.vue | 112 + .../package/refactor/form/flowFormConfig.vue | 332 + .../refactor/form/formEditOperation.vue | 678 + .../refactor/listeners/ElementListeners.vue | 454 + .../refactor/listeners/UserTaskListeners.vue | 486 + .../package/refactor/listeners/utilSelf.ts | 69 + .../multi-instance/ElementMultiInstance.vue | 558 + .../refactor/other/ElementOtherConfig.vue | 65 + .../refactor/properties/ElementProperties.vue | 201 + .../refactor/properties/SetApproveStatus.vue | 111 + .../signal-message/SignalAndMessage.vue | 168 + .../package/refactor/task/ElementTask.vue | 92 + .../task/task-components/ReceiveTask.vue | 141 + .../task/task-components/ScriptTask.vue | 104 + .../task/task-components/UserTask.vue | 957 ++ .../package/theme/flow-element-variables.scss | 64 + .../pages/workflow/package/theme/index.scss | 12 + .../package/theme/process-designer.scss | 252 + .../workflow/package/theme/process-panel.scss | 110 + .../src/pages/workflow/package/utils.ts | 98 + .../workflow/taskManager/formAllInstance.vue | 288 + .../taskManager/formMyApprovedTask.vue | 220 + .../taskManager/formMyHistoryTask.vue | 208 + .../pages/workflow/taskManager/formMyTask.vue | 224 + .../taskManager/formTaskProcessViewer.vue | 90 + .../pages/workflow/taskManager/stopTask.vue | 105 + OrangeFormsOpen-VUE3/src/router/index.ts | 65 + .../src/router/systemRouters.ts | 530 + OrangeFormsOpen-VUE3/src/store/index.ts | 10 + OrangeFormsOpen-VUE3/src/store/layout.ts | 159 + OrangeFormsOpen-VUE3/src/store/login.ts | 31 + OrangeFormsOpen-VUE3/src/store/message.ts | 62 + OrangeFormsOpen-VUE3/src/store/other.ts | 23 + OrangeFormsOpen-VUE3/src/store/utils/index.ts | 109 + .../src/types/auto-import.d.ts | 77 + .../src/types/components.d.ts | 7 + OrangeFormsOpen-VUE3/src/types/generic.d.ts | 9 + .../src/types/online/column.d.ts | 29 + .../src/types/online/dblink.d.ts | 22 + .../src/types/online/dict.d.ts | 37 + .../src/types/online/page.d.ts | 8 + .../src/types/online/table.d.ts | 10 + .../src/types/table/course.d.ts | 34 + .../src/types/table/courseSection.d.ts | 22 + .../src/types/table/teacher.d.ts | 24 + .../src/types/upms/department.d.ts | 26 + OrangeFormsOpen-VUE3/src/types/upms/dict.d.ts | 14 + .../src/types/upms/login.d.ts | 16 + OrangeFormsOpen-VUE3/src/types/upms/menu.d.ts | 26 + OrangeFormsOpen-VUE3/src/types/upms/perm.d.ts | 30 + .../src/types/upms/permcode.d.ts | 13 + .../src/types/upms/permdata.d.ts | 12 + OrangeFormsOpen-VUE3/src/types/upms/post.d.ts | 7 + OrangeFormsOpen-VUE3/src/types/upms/role.d.ts | 5 + OrangeFormsOpen-VUE3/src/types/upms/user.d.ts | 42 + OrangeFormsOpen-VUE3/src/vite-env.d.ts | 29 + OrangeFormsOpen-VUE3/tsconfig.json | 24 + OrangeFormsOpen-VUE3/tsconfig.node.json | 9 + OrangeFormsOpen-VUE3/vite.config.ts | 58 + 1751 files changed, 236790 insertions(+) create mode 100644 OrangeFormsOpen-MybatisFlex/.DS_Store create mode 100644 OrangeFormsOpen-MybatisFlex/.gitignore create mode 100644 OrangeFormsOpen-MybatisFlex/README.md create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application-dev.yml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application.yml create mode 100644 OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/logback-spring.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/.DS_Store create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/PageHelperConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-minio/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-satoken/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-swagger/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java create mode 100644 OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisFlex/common/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/pom.xml create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/.DS_Store create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/db-scripts/.DS_Store create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/db-scripts/zzdemo-online-open.sql create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/.DS_Store create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/docker-compose.yml create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/services/redis/Dockerfile create mode 100644 OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/services/redis/redis.conf create mode 100644 OrangeFormsOpen-MybatisPlus/.DS_Store create mode 100644 OrangeFormsOpen-MybatisPlus/.gitignore create mode 100644 OrangeFormsOpen-MybatisPlus/README.md create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application-dev.yml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application.yml create mode 100644 OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/logback-spring.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/.DS_Store create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/MybatisPlusKeyGeneratorConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-minio/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-satoken/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-swagger/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java create mode 100644 OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 OrangeFormsOpen-MybatisPlus/common/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/pom.xml create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/.DS_Store create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/db-scripts/.DS_Store create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/db-scripts/zzdemo-online-open.sql create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/.DS_Store create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/docker-compose.yml create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/services/redis/Dockerfile create mode 100644 OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/services/redis/redis.conf create mode 100644 OrangeFormsOpen-VUE3/.editorconfig create mode 100644 OrangeFormsOpen-VUE3/.env.development create mode 100644 OrangeFormsOpen-VUE3/.env.production create mode 100644 OrangeFormsOpen-VUE3/.eslintignore create mode 100644 OrangeFormsOpen-VUE3/.eslintrc-auto-import.json create mode 100644 OrangeFormsOpen-VUE3/.eslintrc.cjs create mode 100644 OrangeFormsOpen-VUE3/.gitignore create mode 100644 OrangeFormsOpen-VUE3/.prettierrc.cjs create mode 100644 OrangeFormsOpen-VUE3/.vscode/settings.json create mode 100644 OrangeFormsOpen-VUE3/README.md create mode 100644 OrangeFormsOpen-VUE3/components.d.ts create mode 100644 OrangeFormsOpen-VUE3/index.html create mode 100644 OrangeFormsOpen-VUE3/package-lock.json create mode 100644 OrangeFormsOpen-VUE3/package.json create mode 100644 OrangeFormsOpen-VUE3/public/favicon.ico create mode 100644 OrangeFormsOpen-VUE3/src/App.vue create mode 100644 OrangeFormsOpen-VUE3/src/api/BaseController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/config.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/flow/FlowCategoryController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/flow/FlowDictionaryController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/flow/FlowEntryController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/flow/FlowEntryVariableController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/flow/FlowOperationController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/flow/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineColumnController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceRelationController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineDblinkController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineDictController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineFormController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineOperationController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlinePageController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineRuleController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/OnlineVirtualColumnController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/online/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/DictionaryController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/LoginController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/LoginUserController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/MenuController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/MobileEntryController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/OperationLogController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/PermCodeController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/PermController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/SysCommonBizController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/SysDataPermController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/SysDeptController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/SysGlobalDictController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/SysPostController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/SystemRoleController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/UserController.ts create mode 100644 OrangeFormsOpen-VUE3/src/api/system/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/add.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/advance-add-active.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/advance-add.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/advance-del-active.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/advance-del.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/advance-edit-active.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/advance-edit.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/back.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/back2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/collapse.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/datasource-active.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/datasource.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/default-header.jpg create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/default.jpg create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/demo-h5-qrcode.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/density.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/document-active.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/document.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/down.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/empty.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/eye_close.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/eye_open.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/filter.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/import.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_bg.jpg create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_bg.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_bg2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_icon.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_icon2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_logo.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_logo2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_password.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_title.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/login_username.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/logo.jpg create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/logo.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/logo_white.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/more.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/orange-group1.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/orange-group2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/orange-group3.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/orange-group4.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/orange.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/preview.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/reduce.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/refresh.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/refresh2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/remind.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/resume_icon_add.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/right-icon.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/s-home.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/setting.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/sp1.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/spjd.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/spjd2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/tj.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/tj2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/vant.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/wg.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/img/wg2.png create mode 100644 OrangeFormsOpen-VUE3/src/assets/online-icon/iconfont.css create mode 100644 OrangeFormsOpen-VUE3/src/assets/online-icon/iconfont.ttf create mode 100644 OrangeFormsOpen-VUE3/src/assets/online-icon/iconfont.woff create mode 100644 OrangeFormsOpen-VUE3/src/assets/online-icon/iconfont.woff2 create mode 100644 OrangeFormsOpen-VUE3/src/assets/skin/orange/index.scss create mode 100644 OrangeFormsOpen-VUE3/src/assets/style/base.scss create mode 100644 OrangeFormsOpen-VUE3/src/assets/style/chart.scss create mode 100644 OrangeFormsOpen-VUE3/src/assets/style/form-style.scss create mode 100644 OrangeFormsOpen-VUE3/src/assets/style/index.scss create mode 100644 OrangeFormsOpen-VUE3/src/assets/style/transition.scss create mode 100644 OrangeFormsOpen-VUE3/src/assets/vue.svg create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useCommon.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useDate.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useDownload.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useDropdown.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/usePermission.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useTable.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useUpload.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useUploadWidget.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useUrl.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/hooks/useWindowResize.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/http/axios.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/http/config.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/http/request.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/http/types.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/staticDict/combined.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/staticDict/flow.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/staticDict/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/staticDict/online.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/staticDict/types.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/types/list.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/types/pagination.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/types/sortinfo.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/types/table.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/utils/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/common/utils/validate.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/AdvanceQuery/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/Btns/RightAddBtn.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/DateRange/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/DeptSelect/DeptSelectDlg.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/DeptSelect/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/Dialog/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/Dialog/layout.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/Dialog/types.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/Dialog/useDialog.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/FilterBox/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/IconSelect/icon.json create mode 100644 OrangeFormsOpen-VUE3/src/components/IconSelect/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/InputNumberRange/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/MultiItemBox/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/MultiItemList/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/PageCloseButton/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/Progress/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/RichEditor/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/algorithm/bitmap.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/algorithm/expression.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/assets/material_common_sprite82.svg create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/assets/sprite.svg create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/canvas/draw.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/canvas/draw2.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/border_palette.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/bottombar.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/button.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/calendar.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/color_palette.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/contextmenu.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/datepicker.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_align.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_border.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_color.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_font.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_fontsize.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_format.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_formula.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/dropdown_linetype.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/editor.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/element.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/event.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/form_field.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/form_input.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/form_select.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/icon.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/message.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/modal.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/modal_validation.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/print.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/resizer.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/scrollbar.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/selector.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/sheet.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/sort_filter.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/suggest.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/table.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/align.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/autofilter.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/bold.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/border.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/clearformat.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/dropdown_item.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/fill_color.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/font.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/font_size.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/format.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/formula.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/freeze.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/icon_item.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/index.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/italic.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/item.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/merge.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/more.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/paintformat.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/print.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/redo.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/strike.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/text_color.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/textwrap.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/toggle_item.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/underline.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/undo.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/toolbar/valign.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/component/tooltip.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/config.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/_.prototypes.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/alphabet.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/auto_filter.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/cell.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/cell_range.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/clipboard.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/col.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/data_proxy.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/font.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/format.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/formula.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/helper.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/history.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/merge.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/row.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/scroll.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/selector.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/validation.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/core/validator.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/index.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/index.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/index.scss create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/locale/de.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/locale/en.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/locale/locale.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/locale/nl.js create mode 100644 OrangeFormsOpen-VUE3/src/components/SpreadSheet/locale/zh-cn.js create mode 100644 OrangeFormsOpen-VUE3/src/components/StepBar/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/StepBar/stepItem.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/TableBox/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/TableProgressColumn/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/UserSelect/UserSelectDlg.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/UserSelect/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/icons/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/BreadCrumb.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/Sidebar.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/SubMenu.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/TagItem.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/TagPanel.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/hooks.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/multi-column-menu.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/components/multi-column.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/layout/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/thirdParty/hooks.ts create mode 100644 OrangeFormsOpen-VUE3/src/components/thirdParty/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/components/thirdParty/types.ts create mode 100644 OrangeFormsOpen-VUE3/src/index.scss create mode 100644 OrangeFormsOpen-VUE3/src/main.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/components/ActiveWidgetMenu.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineBaseCard.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCardTable.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomBlock.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomImage.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomLabel.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomTable.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomTabs.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomText.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomTree.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomUpload.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomWidget.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/OnlineCustomWorkFlowTable.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/AttributeCollapse/editWidgetAttribute.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/AttributeCollapse/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/AttributeForm/editWidgetAttribute.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/AttributeForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/CustomWidgetDictSetting/editDictParamValue.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/CustomWidgetDictSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/DateViewTablePagerSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineCustomListOrderSetting/editCustomListOrder.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineCustomListOrderSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineImageUrlInput.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineMobieNumberRangeQuickSelectSetting/editNumberRangeQuick.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineMobieNumberRangeQuickSelectSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineTabPanelSetting/editOnlineTabPanel.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineTabPanelSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineTableColumnSetting/editOnlineTableColumn.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/WidgetAttributeSetting/components/OnlineTableColumnSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/online/components/hooks/widget.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/components/types/widget.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/baseCard.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/cascader.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/checkbox.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/customBlock.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/date.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/dateRange.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/deptSelect.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/image.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/input.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/label.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/link.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/numberInput.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/numberRange.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/radio.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/richEditor.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/select.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/switch.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/table.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/tabs.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/text.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/tree.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/upload.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/userSelect.ts create mode 100644 OrangeFormsOpen-VUE3/src/online/config/workOrderList.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/error/404.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/login/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineAdvanceQueryForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineEditForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineOneToOneForm/OnlineFilterBox.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineOneToOneForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineQueryForm/OnlineFilterBox.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineQueryForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineWorkFlowForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/OnlineWorkOrderForm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/hooks/useForm.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/hooks/useFormExpose.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/OnlinePageRender/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/basic/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/editOnlinePageDatasource.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/editOnlinePageDatasourceRelation.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/editVirtualColumnFilter.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/indev.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/onlinePageTableColumnRule.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/onlinePageVirtualColumn.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/dataModel/setOnlineTableColumnRule.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/editOnlineForm.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/editOnlinePageDatasource.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/editOnlinePageDatasourceRelation.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/editVirtualColumnFilter.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomFormOperateSetting.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomFormSetting.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomTableContainerSetting.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomWidgetAttributeSetting.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomWidgetBindData.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomWidgetDictSetting/EditDictParamValue.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomWidgetDictSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/CustomWidgetRelativeTableSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/EditCustomFormOperate.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/EditFormField.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/EditWidgetAttribute.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/OnlineTabPanelSetting/editOnlineTabPanel.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/OnlineTabPanelSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/OnlineTableColumnSetting/editOnlineTableColumn.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/components/OnlineTableColumnSetting/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/editTableColumn.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/formDesign/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/editOnlinePage/setOnlineTableColumnRule.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/formOnlineDblink/EditOnlineDblink.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/formOnlineDblink/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/formOnlineDict/EditDictDataButton.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/formOnlineDict/EditOnlineDict.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/formOnlineDict/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/formOnlinePage/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/hooks/useDict.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/hooks/useFormConfig.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/online/hooks/useWidgetToolkit.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditDictData/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditGlobalDict/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysDataPerm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysDept/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysMenu/editColumn.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysMenu/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysPerm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysPermCode/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysPermModule/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysPost/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysRole/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formEditSysUser/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDataPerm/TabContentDataPerm.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDataPerm/TabContentDataPermUser.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDataPerm/formSetSysDataPermUser.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDataPerm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDept/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDeptPost/formSetDeptPost.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDeptPost/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysDict/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysLoginUser/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysMenu/formSysColumnMenu.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysMenu/hooks.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysMenu/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysOperationLog/SysOperationLogDetail.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysOperationLog/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPerm/PermGroupList.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPerm/PermList.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPerm/SysPermDetail.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPerm/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPermCode/formSysPermCodeDetail.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPermCode/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysPost/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysRole/TabContentRole.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysRole/TabContentUser.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysRole/formSetRoleUser.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysRole/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/upms/formSysUser/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/welcome/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/welcome/index_bak.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/CopyForSelect/addCopyForItem.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/CopyForSelect/copyForSetting.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/CopyForSelect/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/HandlerFlowTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/ProcessDesigner.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/ProcessViewer.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/TagSelect.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/TaskCommit.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/TaskGroupSelect.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/TaskMultipleSelect.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/TaskPostSelect.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/TaskUserSelect.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/UserTaskSelect/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/components/UserTaskSelect/userTaskSelectDlg.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowCategory/formEditFlowCategory.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowCategory/formFlowCategory.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowEntry/formEditFlowEntry.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowEntry/formEditFlowEntryStatus.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowEntry/formEditFlowEntryVariable.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowEntry/formFlowEntry.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/flowEntry/formPublishedFlowEntry.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/formMessage/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/handlerFlowTask/hook.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/handlerFlowTask/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/handlerFlowTask/types.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/ProcessDesigner.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/highlight.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/content-pad/contentPadProvider.js create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/content-pad/index.js create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/defaultEmpty.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/descriptor/activitiDescriptor.json create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/descriptor/camundaDescriptor.json create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/descriptor/flowableDescriptor.json create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/extension-moddle/activiti/activitiExtension.js create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/extension-moddle/activiti/index.js create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/extension-moddle/camunda/extension.js create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/extension-moddle/camunda/index.js create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/extension-moddle/flowable/flowableExtension.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/extension-moddle/flowable/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/translate/customTranslate.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/process-designer/plugins/translate/zh.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/PropertiesPanel.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/autoAgree/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/base/ElementBaseInfo.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/copy-for/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/flow-condition/FlowCondition.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/form-variable/index.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/form/flowFormConfig.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/form/formEditOperation.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/listeners/ElementListeners.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/listeners/UserTaskListeners.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/listeners/utilSelf.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/multi-instance/ElementMultiInstance.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/other/ElementOtherConfig.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/properties/ElementProperties.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/properties/SetApproveStatus.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/signal-message/SignalAndMessage.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/task/ElementTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/task/task-components/ReceiveTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/task/task-components/ScriptTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/refactor/task/task-components/UserTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/theme/flow-element-variables.scss create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/theme/index.scss create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/theme/process-designer.scss create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/theme/process-panel.scss create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/package/utils.ts create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/taskManager/formAllInstance.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/taskManager/formMyApprovedTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/taskManager/formMyHistoryTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/taskManager/formMyTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/taskManager/formTaskProcessViewer.vue create mode 100644 OrangeFormsOpen-VUE3/src/pages/workflow/taskManager/stopTask.vue create mode 100644 OrangeFormsOpen-VUE3/src/router/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/router/systemRouters.ts create mode 100644 OrangeFormsOpen-VUE3/src/store/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/store/layout.ts create mode 100644 OrangeFormsOpen-VUE3/src/store/login.ts create mode 100644 OrangeFormsOpen-VUE3/src/store/message.ts create mode 100644 OrangeFormsOpen-VUE3/src/store/other.ts create mode 100644 OrangeFormsOpen-VUE3/src/store/utils/index.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/auto-import.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/components.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/generic.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/online/column.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/online/dblink.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/online/dict.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/online/page.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/online/table.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/table/course.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/table/courseSection.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/table/teacher.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/department.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/dict.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/login.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/menu.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/perm.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/permcode.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/permdata.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/post.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/role.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/types/upms/user.d.ts create mode 100644 OrangeFormsOpen-VUE3/src/vite-env.d.ts create mode 100644 OrangeFormsOpen-VUE3/tsconfig.json create mode 100644 OrangeFormsOpen-VUE3/tsconfig.node.json create mode 100644 OrangeFormsOpen-VUE3/vite.config.ts diff --git a/OrangeFormsOpen-MybatisFlex/.DS_Store b/OrangeFormsOpen-MybatisFlex/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5542a45951636ad3b97cf973df2cda7b0a09a150 GIT binary patch literal 6148 zcmeHKu};G<5IsX%3SBxd6ai8;rUDf|5K55PyD-uwRY7W+h*BXThWr8_!0#}#@dXTQ z%uKwqEflA%N+pC4x{K`Rp6{G@Pl{a=k!nw(7Ezsus%VU*4RkY%=ebSUiuG`^(ra{w zgH|U@;&duj9HM|I@b?tpZ?{bQbVU*Msqp9`mXEr>`j z^E<>d(rJ^rl;RnJMTqeYFrg7O=@y)N@I(~DH*$8D^6SAexi0tYzOkdZfBW5WlCNy} ze5da9cdomFY7uP2M>+W zx0o5!TL(IQ1pt=d)`oGN2SCrFZ!t578JIGuK$EKM6+@YH_&povTg(iabW-;6q3oNL zy`d=mcC7E|a8kZO=|uriz*S(?{H*i&e>(sE? + + + com.orangeforms + OrangeFormsOpen + 1.0.0 + + 4.0.0 + + application-webadmin + 1.0.0 + application-webadmin + jar + + + + + com.orangeforms + common-satoken + 1.0.0 + + + com.orangeforms + common-ext + 1.0.0 + + + com.orangeforms + common-redis + 1.0.0 + + + com.orangeforms + common-online + 1.0.0 + + + com.orangeforms + common-flow-online + 1.0.0 + + + com.orangeforms + common-log + 1.0.0 + + + com.orangeforms + common-minio + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-datafilter + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + com.orangeforms + common-dict + 1.0.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java new file mode 100644 index 00000000..86a9458a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java @@ -0,0 +1,23 @@ +package com.orangeforms.webadmin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + * 应用服务启动类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableAsync +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@ComponentScan("com.orangeforms") +public class WebAdminApplication { + + public static void main(String[] args) { + SpringApplication.run(WebAdminApplication.class, args); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java new file mode 100644 index 00000000..d5198b82 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java @@ -0,0 +1,244 @@ +package com.orangeforms.webadmin.app.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.webadmin.upms.model.SysDept; +import com.orangeforms.webadmin.upms.model.SysUser; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.webadmin.upms.model.SysDeptPost; +import com.orangeforms.webadmin.upms.service.SysDeptService; +import com.orangeforms.webadmin.upms.service.SysUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 为流程提供所需的用户身份相关的等扩展信息的帮助类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class FlowIdentityExtHelper implements BaseFlowIdentityExtHelper { + + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysUserService sysUserService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + + @PostConstruct + public void doRegister() { + flowCustomExtFactory.registerFlowIdentityExtHelper(this); + } + + @Override + public Long getLeaderDeptPostId(Long deptId) { + List deptPostIdList = sysDeptService.getLeaderDeptPostIdList(deptId); + return CollUtil.isEmpty(deptPostIdList) ? null : deptPostIdList.get(0); + } + + @Override + public Long getUpLeaderDeptPostId(Long deptId) { + List deptPostIdList = sysDeptService.getUpLeaderDeptPostIdList(deptId); + return CollUtil.isEmpty(deptPostIdList) ? null : deptPostIdList.get(0); + } + + @Override + public Map getDeptPostIdMap(Long deptId, Set postIdSet) { + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + List deptPostList = sysDeptService.getSysDeptPostList(deptId, postIdSet2); + if (CollUtil.isEmpty(deptPostList)) { + return null; + } + Map resultMap = new HashMap<>(deptPostList.size()); + deptPostList.forEach(sysDeptPost -> + resultMap.put(sysDeptPost.getPostId().toString(), sysDeptPost.getDeptPostId().toString())); + return resultMap; + } + + @Override + public Map getSiblingDeptPostIdMap(Long deptId, Set postIdSet) { + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + List deptPostList = sysDeptService.getSiblingSysDeptPostList(deptId, postIdSet2); + if (CollUtil.isEmpty(deptPostList)) { + return null; + } + Map resultMap = new HashMap<>(deptPostList.size()); + for (SysDeptPost deptPost : deptPostList) { + String deptPostId = resultMap.get(deptPost.getPostId().toString()); + if (deptPostId != null) { + deptPostId = deptPostId + "," + deptPost.getDeptPostId(); + } else { + deptPostId = deptPost.getDeptPostId().toString(); + } + resultMap.put(deptPost.getPostId().toString(), deptPostId); + } + return resultMap; + } + + @Override + public Map getUpDeptPostIdMap(Long deptId, Set postIdSet) { + SysDept sysDept = sysDeptService.getById(deptId); + if (sysDept == null || sysDept.getParentId() == null) { + return null; + } + return getDeptPostIdMap(sysDept.getParentId(), postIdSet); + } + + @Override + public Set getUsernameListByRoleIds(Set roleIdSet) { + Set usernameSet = new HashSet<>(); + Set roleIdSet2 = roleIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long roleId : roleIdSet2) { + List userList = sysUserService.getSysUserListByRoleId(roleId, filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByRoleIds(Set roleIdSet) { + List resultList = new LinkedList<>(); + Set roleIdSet2 = roleIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long roleId : roleIdSet2) { + List userList = sysUserService.getSysUserListByRoleId(roleId, filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public Set getUsernameListByDeptIds(Set deptIdSet) { + Set usernameSet = new HashSet<>(); + Set deptIdSet2 = deptIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + for (Long deptId : deptIdSet2) { + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + List userList = sysUserService.getSysUserList(filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByDeptIds(Set deptIdSet) { + List resultList = new LinkedList<>(); + Set deptIdSet2 = deptIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + for (Long deptId : deptIdSet2) { + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + List userList = sysUserService.getSysUserList(filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public Set getUsernameListByPostIds(Set postIdSet) { + Set usernameSet = new HashSet<>(); + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long postId : postIdSet2) { + List userList = sysUserService.getSysUserListByPostId(postId, filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByPostIds(Set postIdSet) { + List resultList = new LinkedList<>(); + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long postId : postIdSet2) { + List userList = sysUserService.getSysUserListByPostId(postId, filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public Set getUsernameListByDeptPostIds(Set deptPostIdSet) { + Set usernameSet = new HashSet<>(); + Set deptPostIdSet2 = deptPostIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long deptPostId : deptPostIdSet2) { + List userList = sysUserService.getSysUserListByDeptPostId(deptPostId, filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByDeptPostIds(Set deptPostIdSet) { + List resultList = new LinkedList<>(); + Set deptPostIdSet2 = deptPostIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long deptPostId : deptPostIdSet2) { + List userList = sysUserService.getSysUserListByDeptPostId(deptPostId, filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public List getUserInfoListByUsernameSet(Set usernameSet) { + List resultList = null; + List userList = sysUserService.getInList("loginName", usernameSet); + if (CollUtil.isNotEmpty(userList)) { + resultList = BeanUtil.copyToList(userList, FlowUserInfoVo.class); + } + return resultList; + } + + @Override + public Boolean supprtDataPerm() { + return true; + } + + @Override + public Map mapUserShowNameByLoginName(Set loginNameSet) { + if (CollUtil.isEmpty(loginNameSet)) { + return new HashMap<>(1); + } + Map resultMap = new HashMap<>(loginNameSet.size()); + List userList = sysUserService.getInList("loginName", loginNameSet); + userList.forEach(user -> resultMap.put(user.getLoginName(), user.getShowName())); + return resultMap; + } + + private void extractAndAppendUsernameList(Set resultUsernameList, List userList) { + List usernameList = userList.stream().map(SysUser::getLoginName).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(usernameList)) { + resultUsernameList.addAll(usernameList); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java new file mode 100644 index 00000000..dd028f9d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java @@ -0,0 +1,38 @@ +package com.orangeforms.webadmin.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 应用程序自定义的程序属性配置文件。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "application") +public class ApplicationConfig { + /** + * 用户密码被重置之后的缺省密码 + */ + private String defaultUserPassword; + /** + * 上传文件的基础目录 + */ + private String uploadFileBaseDir; + /** + * 授信ip列表,没有填写表示全部信任。多个ip之间逗号分隔,如: http://10.10.10.1:8080,http://10.10.10.2:8080 + */ + private String credentialIpList; + /** + * Session的用户权限在Redis中的过期时间(秒)。一定要和sa-token.timeout + * 缺省值是 one day + */ + private int sessionExpiredSeconds = 86400; + /** + * 是否排他登录。 + */ + private Boolean excludeLogin = false; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java new file mode 100644 index 00000000..4820bda3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.config; + +import com.orangeforms.common.core.constant.ApplicationConstant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表示数据源类型的常量对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DataSourceType { + + public static final int MAIN = 0; + /** + * 以下所有数据源的类都型是固定值。如果有冲突,请修改上面定义的业务服务的数据源类型值。 + */ + public static final int OPERATION_LOG = ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE; + public static final int GLOBAL_DICT = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE; + public static final int COMMON_FLOW_AND_ONLINE = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE; + + private static final Map TYPE_MAP = new HashMap<>(8); + static { + TYPE_MAP.put("main", MAIN); + TYPE_MAP.put("operation-log", OPERATION_LOG); + TYPE_MAP.put("global-dict", GLOBAL_DICT); + TYPE_MAP.put("common-flow-online", COMMON_FLOW_AND_ONLINE); + } + + /** + * 根据名称获取字典类型。 + * + * @param name 数据源在配置中的名称。 + * @return 返回可用于多数据源切换的数据源类型。 + */ + public static Integer getDataSourceTypeByName(String name) { + return TYPE_MAP.get(name); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataSourceType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java new file mode 100644 index 00000000..350602db --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java @@ -0,0 +1,60 @@ +package com.orangeforms.webadmin.config; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import jakarta.servlet.Filter; +import java.nio.charset.StandardCharsets; + +/** + * 这里主要配置Web的各种过滤器和监听器等Servlet容器组件。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class FilterConfig { + + /** + * 配置Ajax跨域过滤器。 + */ + @Bean + public CorsFilter corsFilterRegistration(ApplicationConfig applicationConfig) { + UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + if (StringUtils.isNotBlank(applicationConfig.getCredentialIpList())) { + if ("*".equals(applicationConfig.getCredentialIpList())) { + corsConfiguration.addAllowedOriginPattern("*"); + } else { + String[] credentialIpList = StringUtils.split(applicationConfig.getCredentialIpList(), ","); + if (credentialIpList.length > 0) { + for (String ip : credentialIpList) { + corsConfiguration.addAllowedOrigin(ip); + } + } + } + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + corsConfiguration.setAllowCredentials(true); + configSource.registerCorsConfiguration("/**", corsConfiguration); + } + return new CorsFilter(configSource); + } + + @Bean + public FilterRegistrationBean characterEncodingFilterRegistration() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new org.springframework.web.filter.CharacterEncodingFilter()); + filterRegistrationBean.addUrlPatterns("/*"); + filterRegistrationBean.addInitParameter("encoding", StandardCharsets.UTF_8.name()); + // forceEncoding强制response也被编码,另外即使request中已经设置encoding,forceEncoding也会重新设置 + filterRegistrationBean.addInitParameter("forceEncoding", "true"); + filterRegistrationBean.setAsyncSupported(true); + return filterRegistrationBean; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java new file mode 100644 index 00000000..1d75ac6d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java @@ -0,0 +1,21 @@ +package com.orangeforms.webadmin.config; + +import com.orangeforms.webadmin.interceptor.AuthenticationInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 所有的项目拦截器都在这里集中配置 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/admin/**"); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java new file mode 100644 index 00000000..bb09bf79 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java @@ -0,0 +1,77 @@ +package com.orangeforms.webadmin.config; + +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.orangeforms.common.core.config.BaseMultiDataSourceConfig; +import com.orangeforms.common.core.config.DynamicDataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.mybatis.spring.annotation.MapperScan; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/** + * 多数据源配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@EnableTransactionManagement +@MapperScan(value = {"com.orangeforms.webadmin.*.dao", "com.orangeforms.common.*.dao"}) +public class MultiDataSourceConfig extends BaseMultiDataSourceConfig { + + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.main") + public DataSource mainDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + /** + * 默认生成的用于保存操作日志的数据源,可根据需求修改。 + * 这里我们还是非常推荐给操作日志使用独立的数据源,这样便于今后的数据迁移。 + */ + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.operation-log") + public DataSource operationLogDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + /** + * 默认生成的用于全局编码字典的数据源,可根据需求修改。 + */ + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.global-dict") + public DataSource globalDictDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + /** + * 默认生成的用于在线表单内部表的数据源,可根据需求修改。 + * 这里我们还是非常推荐使用独立数据源,这样便于今后的服务拆分。 + */ + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.common-flow-online") + public DataSource commonFlowAndOnlineDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + @Bean + @Primary + public DynamicDataSource dataSource() { + Map targetDataSources = new HashMap<>(1); + targetDataSources.put(DataSourceType.MAIN, mainDataSource()); + targetDataSources.put(DataSourceType.OPERATION_LOG, operationLogDataSource()); + targetDataSources.put(DataSourceType.GLOBAL_DICT, globalDictDataSource()); + targetDataSources.put(DataSourceType.COMMON_FLOW_AND_ONLINE, commonFlowAndOnlineDataSource()); + // 如果当前工程支持在线表单,这里请务必保证upms数据表所在数据库为缺省数据源。 + DynamicDataSource dynamicDataSource = new DynamicDataSource(); + dynamicDataSource.setTargetDataSources(targetDataSources); + dynamicDataSource.setDefaultTargetDataSource(mainDataSource()); + return dynamicDataSource; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java new file mode 100644 index 00000000..e827057a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java @@ -0,0 +1,66 @@ +package com.orangeforms.webadmin.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.Data; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 第三方应用鉴权配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "third-party") +public class ThirdPartyAuthConfig implements InitializingBean { + + private List auth; + + private Map applicationMap; + + @Override + public void afterPropertiesSet() throws Exception { + if (CollUtil.isEmpty(auth)) { + applicationMap = new HashMap<>(1); + } else { + applicationMap = auth.stream().collect(Collectors.toMap(AuthProperties::getAppCode, c -> c)); + } + } + + @Data + public static class AuthProperties { + /** + * 应用Id。 + */ + private String appCode; + /** + * 身份验证相关url的base地址。 + */ + private String baseUrl; + /** + * 是否为橙单框架。 + */ + private Boolean orangeFramework = true; + /** + * token的Http Request Header的key + */ + private String tokenHeaderKey; + /** + * 数据权限和用户操作权限缓存过期时间,单位秒。 + */ + private Integer permExpiredSeconds = 86400; + /** + * 用户Token缓存过期时间,单位秒。 + * 如果为0,则每次都要去第三方服务进行验证。 + */ + private Integer tokenExpiredSeconds = 0; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java new file mode 100644 index 00000000..f2329ff6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java @@ -0,0 +1,281 @@ +package com.orangeforms.webadmin.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.DataPermRuleType; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.satoken.util.SaTokenUtil; +import com.orangeforms.webadmin.config.ThirdPartyAuthConfig; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 登录用户Token验证、生成和权限验证的拦截器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class AuthenticationInterceptor implements HandlerInterceptor { + + private final ThirdPartyAuthConfig thirdPartyAuthConfig = + ApplicationContextHolder.getBean("thirdPartyAuthConfig"); + + private final RedissonClient redissonClient = ApplicationContextHolder.getBean(RedissonClient.class); + private final CacheManager cacheManager = ApplicationContextHolder.getBean("caffeineCacheManager"); + + private final SaTokenUtil saTokenUtil = + ApplicationContextHolder.getBean("saTokenUtil"); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String appCode = this.getAppCodeFromRequest(request); + if (StrUtil.isNotBlank(appCode)) { + return this.handleThirdPartyRequest(appCode, request); + } + ResponseResult result = saTokenUtil.handleAuthIntercept(request, handler); + if (!result.isSuccess()) { + ResponseResult.output(result.getHttpStatus(), result); + return false; + } + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + // 这里需要空注解,否则sonar会不happy。 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + // 这里需要空注解,否则sonar会不happy。 + } + + private String getTokenFromRequest(HttpServletRequest request, String appCode) { + ThirdPartyAuthConfig.AuthProperties prop = thirdPartyAuthConfig.getApplicationMap().get(appCode); + String token = request.getHeader(prop.getTokenHeaderKey()); + if (StrUtil.isBlank(token)) { + token = request.getParameter(prop.getTokenHeaderKey()); + } + if (StrUtil.isBlank(token)) { + token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN); + } + return token; + } + + private String getAppCodeFromRequest(HttpServletRequest request) { + String appCode = request.getHeader("AppCode"); + if (StrUtil.isBlank(appCode)) { + appCode = request.getParameter("AppCode"); + } + return appCode; + } + + private boolean handleThirdPartyRequest(String appCode, HttpServletRequest request) throws IOException { + String token = this.getTokenFromRequest(request, appCode); + ThirdPartyAuthConfig.AuthProperties authProps = thirdPartyAuthConfig.getApplicationMap().get(appCode); + if (authProps == null) { + String msg = StrFormatter.format("请求的 appCode[{}] 信息,在当前服务中尚未配置!", appCode); + ResponseResult.output(ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, msg)); + return false; + } + ResponseResult result = this.getAndCacheThirdPartyTokenData(authProps, token); + if (!result.isSuccess()) { + ResponseResult.output(result.getHttpStatus(), + ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, result.getErrorMessage())); + return false; + } + TokenData tokenData = result.getData(); + tokenData.setAppCode(appCode); + tokenData.setSessionId(this.prependAppCode(authProps.getAppCode(), tokenData.getSessionId())); + TokenData.addToRequest(tokenData); + String url = request.getRequestURI(); + if (Boolean.FALSE.equals(tokenData.getIsAdmin()) + && !this.hasThirdPartyPermission(authProps, tokenData, url)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return false; + } + return true; + } + + private ResponseResult getAndCacheThirdPartyTokenData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + if (authProps.getTokenExpiredSeconds() == 0) { + return this.getThirdPartyTokenData(authProps, token); + } + String tokeKey = this.prependAppCode(authProps.getAppCode(), RedisKeyUtil.makeSessionIdKey(token)); + RBucket sessionData = redissonClient.getBucket(tokeKey); + if (sessionData.isExists()) { + return ResponseResult.success(JSON.parseObject(sessionData.get(), TokenData.class)); + } + ResponseResult responseResult = this.getThirdPartyTokenData(authProps, token); + if (responseResult.isSuccess()) { + sessionData.set(JSON.toJSONString(responseResult.getData()), authProps.getTokenExpiredSeconds(), TimeUnit.SECONDS); + } + return responseResult; + } + + private String prependAppCode(String appCode, String key) { + return appCode.toUpperCase() + ":" + key; + } + + private ResponseResult getThirdPartyTokenData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + try { + String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getTokenData", token); + return JSON.parseObject(resultData, new TypeReference>() {}); + } catch (MyRuntimeException ex) { + return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage()); + } + } + + private ResponseResult getThirdPartyPermData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + try { + String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getPermData", token); + return JSON.parseObject(resultData, new TypeReference>() {}); + } catch (MyRuntimeException ex) { + return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage()); + } + } + + private String invokeThirdPartyUrl(String url, String token) { + Map headerMap = new HashMap<>(1); + headerMap.put("Authorization", token); + StringBuilder fullUrl = new StringBuilder(128); + fullUrl.append(url).append("?token=").append(token); + HttpResponse httpResponse = HttpUtil.createGet(fullUrl.toString()).addHeaders(headerMap).execute(); + if (!httpResponse.isOk()) { + String msg = StrFormatter.format( + "Failed to call [{}] with ERROR HTTP Status [{}] and [{}].", + url, httpResponse.getStatus(), httpResponse.body()); + log.error(msg); + throw new MyRuntimeException(msg); + } + return httpResponse.body(); + } + + @SuppressWarnings("unchecked") + private boolean hasThirdPartyPermission( + ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, String url) { + // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。 + String permKey = RedisKeyUtil.makeSessionPermIdKey(tokenData.getSessionId()); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL"); + Cache.ValueWrapper wrapper = cache.get(permKey); + if (wrapper != null) { + Object cachedData = wrapper.get(); + if (cachedData != null) { + return ((Set) cachedData).contains(url); + } + } + Set localPermSet; + RSet permSet = redissonClient.getSet(permKey); + if (permSet.isExists()) { + localPermSet = permSet.readAll(); + cache.put(permKey, localPermSet); + return localPermSet.contains(url); + } + ResponseResult responseResult = this.getThirdPartyPermData(authProps, tokenData.getToken()); + this.cacheThirdPartyDataPermData(authProps, tokenData, responseResult.getData().getDataPerms()); + if (CollUtil.isEmpty(responseResult.getData().urlPerms)) { + return false; + } + permSet.addAll(responseResult.getData().urlPerms); + permSet.expire(authProps.getPermExpiredSeconds(), TimeUnit.SECONDS); + localPermSet = new HashSet<>(responseResult.getData().urlPerms); + cache.put(permKey, localPermSet); + return localPermSet.contains(url); + } + + private void cacheThirdPartyDataPermData( + ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, List dataPerms) { + if (CollUtil.isEmpty(dataPerms)) { + return; + } + Map> dataPermMap = + dataPerms.stream().collect(Collectors.groupingBy(ThirdPartyAppDataPermData::getRuleType)); + Map> normalizedDataPermMap = new HashMap<>(dataPermMap.size()); + for (Map.Entry> entry : dataPermMap.entrySet()) { + List ruleTypeDataPermDataList; + if (entry.getKey().equals(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) { + ruleTypeDataPermDataList = + normalizedDataPermMap.computeIfAbsent(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, k -> new LinkedList<>()); + } else { + ruleTypeDataPermDataList = + normalizedDataPermMap.computeIfAbsent(entry.getKey(), k -> new LinkedList<>()); + } + ruleTypeDataPermDataList.addAll(entry.getValue()); + } + Map resultDataPermMap = new HashMap<>(normalizedDataPermMap.size()); + for (Map.Entry> entry : normalizedDataPermMap.entrySet()) { + if (entry.getKey().equals(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST)) { + String deptIds = entry.getValue().stream() + .map(ThirdPartyAppDataPermData::getDeptIds).collect(Collectors.joining(",")); + resultDataPermMap.put(entry.getKey(), deptIds); + } else { + resultDataPermMap.put(entry.getKey(), "null"); + } + } + Map> menuDataPermMap = new HashMap<>(1); + menuDataPermMap.put(ApplicationConstant.DATA_PERM_ALL_MENU_ID, resultDataPermMap); + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + RBucket bucket = redissonClient.getBucket(dataPermSessionKey); + bucket.set(JSON.toJSONString(menuDataPermMap), authProps.getPermExpiredSeconds(), TimeUnit.SECONDS); + } + + @Data + public static class ThirdPartyAppPermData { + /** + * 当前用户会话可访问的url接口地址列表。 + */ + private List urlPerms; + /** + * 当前用户会话的数据权限列表。 + */ + private List dataPerms; + } + + @Data + public static class ThirdPartyAppDataPermData { + /** + * 数据权限的规则类型。需要按照橙单的约定返回。具体值可参考DataPermRuleType常量类。 + */ + private Integer ruleType; + /** + * 部门Id集合,多个部门Id之间逗号分隔。 + * 注意:仅当ruleType为3、4、5时需要包含该字段值。 + */ + private String deptIds; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java new file mode 100644 index 00000000..dbca8a5b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java @@ -0,0 +1,55 @@ +package com.orangeforms.webadmin.upms.bo; + +import lombok.Data; + +import java.util.List; + +/** + * 菜单扩展数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SysMenuExtraData { + + /** + * 路由名称。 + */ + private String formRouterName; + + /** + * 在线表单。 + */ + private Long onlineFormId; + + /** + * 报表页面。 + */ + private Long reportPageId; + + /** + * 流程。 + */ + private Long onlineFlowEntryId; + + /** + * 目标url。 + */ + private String targetUrl; + + /** + * 绑定类型。 + */ + private Integer bindType; + + /** + * 前端使用的菜单编码。仅当选择satoken权限框架时使用。 + */ + private String menuCode; + + /** + * 菜单关联的后台使用的权限字列表。仅当选择satoken权限框架时使用。 + */ + private List permCodeList; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java new file mode 100644 index 00000000..8c429d37 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java @@ -0,0 +1,66 @@ +package com.orangeforms.webadmin.upms.bo; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 菜单相关的业务对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SysMenuPerm { + + /** + * 菜单Id。 + */ + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null + */ + private Long parentId; + + /** + * 菜单显示名称。 + */ + private String menuName; + + /** + * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + private Integer menuType; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + private Long onlineFormId; + + /** + * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。 + */ + private Integer onlineMenuPermType; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + private Long onlineFlowEntryId; + + /** + * 关联权限URL集合。 + */ + Set permUrlSet = new HashSet<>(); + + /** + * 关联的某一个url。 + */ + String url; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java new file mode 100644 index 00000000..df90d312 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java @@ -0,0 +1,340 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dict.dto.GlobalDictDto; +import com.orangeforms.common.dict.dto.GlobalDictItemDto; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictItemService; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.dict.util.GlobalDictOperationHelper; +import com.orangeforms.common.dict.vo.GlobalDictVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 全局通用字典操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "全局字典管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/globalDict") +public class GlobalDictController { + + @Autowired + private GlobalDictService globalDictService; + @Autowired + private GlobalDictItemService globalDictItemService; + @Autowired + private GlobalDictOperationHelper globalDictOperationHelper; + + /** + * 新增全局字典接口。 + * + * @param globalDictDto 新增字典对象。 + * @return 保存后的字典对象。 + */ + @ApiOperationSupport(ignoreParameters = {"globalDictDto.dictId"}) + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody GlobalDictDto globalDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 这里必须手动校验字典编码是否存在,因为我们缺省的实现是逻辑删除,所以字典编码字段没有设置为唯一索引。 + if (globalDictService.existDictCode(globalDictDto.getDictCode())) { + errorMessage = "数据验证失败,字典编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDict globalDict = MyModelUtil.copyTo(globalDictDto, GlobalDict.class); + globalDictService.saveNew(globalDict); + return ResponseResult.success(globalDict.getDictId()); + } + + /** + * 更新全局字典操作。 + * + * @param globalDictDto 更新全局字典对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody GlobalDictDto globalDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDict originalGlobalDict = globalDictService.getById(globalDictDto.getDictId()); + if (originalGlobalDict == null) { + errorMessage = "数据验证失败,当前全局字典并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + GlobalDict globalDict = MyModelUtil.copyTo(globalDictDto, GlobalDict.class); + if (ObjectUtil.notEqual(globalDict.getDictCode(), originalGlobalDict.getDictCode()) + && globalDictService.existDictCode(globalDict.getDictCode())) { + errorMessage = "数据验证失败,字典编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!globalDictService.update(globalDict, originalGlobalDict)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除指定的全局字典。 + * + * @param dictId 指定全局字典主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody(required = true) Long dictId) { + if (!globalDictService.remove(dictId)) { + String errorMessage = "数据操作失败,全局字典Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 查看全局字典列表。 + * + * @param globalDictDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含角色列表。 + */ + @SaCheckPermission("globalDict.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody GlobalDictDto globalDictDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + GlobalDict filter = MyModelUtil.copyTo(globalDictDtoFilter, GlobalDict.class); + List globalDictList = + globalDictService.getGlobalDictList(filter, MyOrderParam.buildOrderBy(orderParam, GlobalDict.class)); + List globalDictVoList = + MyModelUtil.copyCollectionTo(globalDictList, GlobalDictVo.class); + long totalCount = 0L; + if (globalDictList instanceof Page) { + totalCount = ((Page) globalDictList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(globalDictVoList, totalCount)); + } + + /** + * 新增全局字典项目接口。 + * + * @param globalDictItemDto 新增字典项目对象。 + * @return 保存后的字典对象。 + */ + @SaCheckPermission("globalDict.update") + @ApiOperationSupport(ignoreParameters = {"globalDictItemDto.id"}) + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/addItem") + public ResponseResult addItem(@MyRequestBody GlobalDictItemDto globalDictItemDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictItemDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!globalDictService.existDictCode(globalDictItemDto.getDictCode())) { + errorMessage = "数据验证失败,字典编码不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (globalDictItemService.existDictCodeAndItemId( + globalDictItemDto.getDictCode(), globalDictItemDto.getItemId())) { + errorMessage = "数据验证失败,该字典编码的项目Id已存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDictItem globalDictItem = MyModelUtil.copyTo(globalDictItemDto, GlobalDictItem.class); + globalDictItemService.saveNew(globalDictItem); + return ResponseResult.success(globalDictItem.getId()); + } + + /** + * 更新全局字典项目。 + * + * @param globalDictItemDto 更新全局字典项目对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateItem") + public ResponseResult updateItem(@MyRequestBody GlobalDictItemDto globalDictItemDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictItemDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDictItem originalGlobalDictItem = globalDictItemService.getById(globalDictItemDto.getId()); + if (originalGlobalDictItem == null) { + errorMessage = "数据验证失败,当前全局字典项目并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + GlobalDictItem globalDictItem = MyModelUtil.copyTo(globalDictItemDto, GlobalDictItem.class); + if (ObjectUtil.notEqual(globalDictItem.getDictCode(), originalGlobalDictItem.getDictCode())) { + errorMessage = "数据验证失败,字典项目的字典编码不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(globalDictItem.getItemId(), originalGlobalDictItem.getItemId()) + && globalDictItemService.existDictCodeAndItemId(globalDictItem.getDictCode(), globalDictItem.getItemId())) { + errorMessage = "数据验证失败,该字典编码已经包含了该项目Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!globalDictItemService.update(globalDictItem, originalGlobalDictItem)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 更新全局字典项目的状态。 + * + * @param id 更新全局字典项目主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateItemStatus") + public ResponseResult updateItemStatus( + @MyRequestBody(required = true) Long id, @MyRequestBody(required = true) Integer status) { + String errorMessage; + GlobalDictItem dictItem = globalDictItemService.getById(id); + if (dictItem == null) { + errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (ObjectUtil.notEqual(dictItem.getStatus(), status)) { + globalDictItemService.updateStatus(dictItem, status); + } + return ResponseResult.success(); + } + + /** + * 删除指定编码的全局字典项目。 + * + * @param id 主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/deleteItem") + public ResponseResult deleteItem(@MyRequestBody(required = true) Long id) { + String errorMessage; + GlobalDictItem dictItem = globalDictItemService.getById(id); + if (dictItem == null) { + errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!globalDictItemService.remove(dictItem)) { + errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 将当前字典表的数据重新加载到缓存中。 + * 由于缓存的数据更新,在add/update/delete等接口均有同步处理。因此该接口仅当同步过程中出现问题时, + * 可手工调用,或者每天晚上定时同步一次。 + */ + @SaCheckPermission("globalDict.view") + @OperationLog(type = SysOperationLogType.RELOAD_CACHE) + @GetMapping("/reloadCachedData") + public ResponseResult reloadCachedData(@RequestParam String dictCode) { + globalDictService.reloadCachedData(dictCode); + return ResponseResult.success(true); + } + + /** + * 获取指定字典编码的全局字典项目。字典的键值为[itemId, itemName]。 + * NOTE: 白名单接口。 + * + * @param dictCode 字典编码。 + * @param itemIdType 字典项目的ItemId值转换到的目标类型。可能值为Integer或Long。 + * @return 应答结果对象。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict( + @RequestParam String dictCode, @RequestParam(required = false) String itemIdType) { + List resultList = + globalDictService.getGlobalDictItemListFromCache(dictCode, null); + resultList = resultList.stream() + .sorted(Comparator.comparing(GlobalDictItem::getStatus)) + .sorted(Comparator.comparing(GlobalDictItem::getShowOrder)) + .collect(Collectors.toList()); + return ResponseResult.success(globalDictOperationHelper.toDictDataList(resultList, itemIdType)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * NOTE: 白名单接口。 + * + * @param dictCode 字典编码。 + * @param itemIds 字典项目Id集合。 + * @param itemIdType 字典项目的ItemId值转换到的目标类型。可能值为Integer或Long。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds( + @RequestParam String dictCode, + @RequestParam List itemIds, + @RequestParam(required = false) String itemIdType) { + List resultList = + globalDictService.getGlobalDictItemListFromCache(dictCode, new HashSet<>(itemIds)); + return ResponseResult.success(globalDictOperationHelper.toDictDataList(resultList, itemIdType)); + } + + /** + * 白名单接口,登录用户均可访问。以字典形式返回全部字典数据集合。 + * fullResultList中的字典列表全部取自于数据库,而cachedResultList全部取自于缓存,前端负责比对。 + * + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listAll") + public ResponseResult listAll(@RequestParam String dictCode) { + List fullResultList = + globalDictItemService.getGlobalDictItemListByDictCode(dictCode); + List cachedList = + globalDictService.getGlobalDictItemListFromCache(dictCode, null); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("fullResultList", globalDictOperationHelper.toDictDataList2(fullResultList)); + jsonObject.put("cachedResultList", globalDictOperationHelper.toDictDataList2(cachedList)); + return ResponseResult.success(jsonObject); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java new file mode 100644 index 00000000..656c9a38 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java @@ -0,0 +1,475 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONArray; +import com.github.xiaoymin.knife4j.annotations.ApiSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.config.ApplicationConfig; +import com.orangeforms.webadmin.upms.bo.SysMenuExtraData; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.webadmin.upms.model.constant.SysUserType; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import com.orangeforms.webadmin.upms.model.constant.SysOnlineMenuPermType; +import com.orangeforms.common.flow.online.service.FlowOnlineOperationService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.upload.*; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.satoken.util.SaTokenUtil; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 登录接口控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@ApiSupport(order = 1) +@Tag(name = "用户登录接口") +@DisableDataFilter +@Slf4j +@RestController +@RequestMapping("/admin/upms/login") +public class LoginController { + + @Autowired + private SysUserService sysUserService; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysMenuService sysMenuService; + @Autowired + private SysPostService sysPostService; + @Autowired + private SysRoleService sysRoleService; + @Autowired + private SysDataPermService sysDataPermService; + @Autowired + private SysPermWhitelistService sysPermWhitelistService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private FlowOnlineOperationService flowOnlineOperationService; + @Autowired + private ApplicationConfig appConfig; + @Autowired + private RedissonClient redissonClient; + @Autowired + private SessionCacheHelper cacheHelper; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private SaTokenUtil saTokenUtil; + + private static final String IS_ADMIN = "isAdmin"; + private static final String SHOW_NAME_FIELD = "showName"; + private static final String SHOW_ORDER_FIELD = "showOrder"; + private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl"; + + /** + * 登录接口。 + * + * @param loginName 登录名。 + * @param password 密码。 + * @return 应答结果对象,其中包括Token数据,以及菜单列表。 + */ + @Parameter(name = "loginName", example = "admin") + @Parameter(name = "password", example = "IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D") + @SaIgnore + @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false) + @PostMapping("/doLogin") + public ResponseResult doLogin( + @MyRequestBody String loginName, + @MyRequestBody String password) throws UnsupportedEncodingException { + if (MyCommonUtil.existBlankArgument(loginName, password)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.verifyAndHandleLoginUser(loginName, password); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + JSONObject jsonData = this.buildLoginDataAndLogin(verifyResult.getData()); + return ResponseResult.success(jsonData); + } + + /** + * 登出操作。同时将Session相关的信息从缓存中删除。 + * + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.LOGOUT) + @PostMapping("/doLogout") + public ResponseResult doLogout() { + String sessionId = TokenData.takeFromRequest().getSessionId(); + redissonClient.getBucket(TokenData.takeFromRequest().getMySessionId()).deleteAsync(); + redissonClient.getBucket(RedisKeyUtil.makeSessionPermCodeKey(sessionId)).deleteAsync(); + redissonClient.getBucket(RedisKeyUtil.makeSessionPermIdKey(sessionId)).deleteAsync(); + sysDataPermService.removeDataPermCache(sessionId); + cacheHelper.removeAllSessionCache(sessionId); + StpUtil.logout(); + return ResponseResult.success(); + } + + /** + * 在登录之后,通过token再次获取登录信息。 + * 用于在当前浏览器登录系统后,在新tab页中可以免密登录。 + * + * @return 应答结果对象,其中包括JWT的Token数据,以及菜单列表。 + */ + @GetMapping("/getLoginInfo") + public ResponseResult getLoginInfo() { + TokenData tokenData = TokenData.takeFromRequest(); + JSONObject jsonData = new JSONObject(); + jsonData.put(SHOW_NAME_FIELD, tokenData.getShowName()); + jsonData.put(IS_ADMIN, tokenData.getIsAdmin()); + if (StrUtil.isNotBlank(tokenData.getHeadImageUrl())) { + jsonData.put(HEAD_IMAGE_URL_FIELD, tokenData.getHeadImageUrl()); + } + Collection allMenuList; + if (BooleanUtil.isTrue(tokenData.getIsAdmin())) { + allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD); + } else { + allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds()); + } + List menuCodeList = new LinkedList<>(); + OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet); + OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet); + allMenuList.stream().filter(m -> m.getExtraData() != null) + .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class))); + this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList); + return ResponseResult.success(jsonData); + } + + /** + * 返回所有可用的权限字列表。 + * + * @return 整个系统所有可用的权限字列表。 + */ + @GetMapping("/getAllPermCodes") + public ResponseResult> getAllPermCodes() { + List permCodes = saTokenUtil.getAllPermCodes(); + return ResponseResult.success(permCodes); + } + + /** + * 用户修改自己的密码。 + * + * @param oldPass 原有密码。 + * @param newPass 新密码。 + * @return 应答结果对象。 + */ + @PostMapping("/changePassword") + public ResponseResult changePassword( + @MyRequestBody String oldPass, @MyRequestBody String newPass) throws UnsupportedEncodingException { + if (MyCommonUtil.existBlankArgument(newPass, oldPass)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + SysUser user = sysUserService.getById(tokenData.getUserId()); + oldPass = URLDecoder.decode(oldPass, StandardCharsets.UTF_8.name()); + // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。 + // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。 + oldPass = RsaUtil.decrypt(oldPass, ApplicationConstant.PRIVATE_KEY); + if (user == null || !passwordEncoder.matches(oldPass, user.getPassword())) { + return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD); + } + newPass = URLDecoder.decode(newPass, StandardCharsets.UTF_8.name()); + newPass = RsaUtil.decrypt(newPass, ApplicationConstant.PRIVATE_KEY); + if (!sysUserService.changePassword(tokenData.getUserId(), newPass)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 上传并修改用户头像。 + * + * @param uploadFile 上传的头像文件。 + */ + @PostMapping("/changeHeadImage") + public void changeHeadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD); + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, true, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + responseInfo.setDownloadUri("/admin/upms/login/downloadHeadImage"); + String newHeadImage = JSONArray.toJSONString(CollUtil.newArrayList(responseInfo)); + if (!sysUserService.changeHeadImage(TokenData.takeFromRequest().getUserId(), newHeadImage)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST)); + return; + } + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + /** + * 下载用户头像。 + * + * @param filename 文件名。如果没有提供该参数,就从当前记录的指定字段中读取。 + * @param response Http 应答对象。 + */ + @GetMapping("/downloadHeadImage") + public void downloadHeadImage(String filename, HttpServletResponse response) { + try { + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD); + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + upDownloader.doDownload(appConfig.getUploadFileBaseDir(), + SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, filename, true, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + private ResponseResult verifyAndHandleLoginUser( + String loginName, String password) throws UnsupportedEncodingException { + String errorMessage; + SysUser user = sysUserService.getSysUserByLoginName(loginName); + password = URLDecoder.decode(password, StandardCharsets.UTF_8.name()); + // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。 + // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。 + password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY); + if (user == null || !passwordEncoder.matches(password, user.getPassword())) { + return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD); + } + if (user.getUserStatus() == SysUserStatus.STATUS_LOCKED) { + errorMessage = "登录失败,用户账号被锁定!"; + return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage); + } + if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) { + String deviceType = MyCommonUtil.getDeviceTypeWithString(); + LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class); + String loginId = SaTokenUtil.makeLoginId(userInfo); + StpUtil.kickout(loginId, deviceType); + } + return ResponseResult.success(user); + } + + private JSONObject buildLoginDataAndLogin(SysUser user) { + TokenData tokenData = this.loginAndCreateToken(user); + // 这里手动将TokenData存入request,便于OperationLogAspect统一处理操作日志。 + TokenData.addToRequest(tokenData); + JSONObject jsonData = this.createResponseData(user); + Collection allMenuList; + boolean isAdmin = user.getUserType() == SysUserType.TYPE_ADMIN; + if (isAdmin) { + allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD); + } else { + allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds()); + } + allMenuList.stream().filter(m -> m.getExtraData() != null) + .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class))); + Collection permCodeList = new LinkedList<>(); + allMenuList.stream().filter(m -> m.getExtraObject() != null) + .forEach(m -> CollUtil.addAll(permCodeList, m.getExtraObject().getPermCodeList())); + Set permSet = new HashSet<>(); + if (!isAdmin) { + // 所有登录用户都有白名单接口的访问权限。 + CollUtil.addAll(permSet, sysPermWhitelistService.getWhitelistPermList()); + } + List menuCodeList = new LinkedList<>(); + OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet); + OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet); + if (!isAdmin) { + permSet.addAll(onlinePermData.permUrlSet); + permSet.addAll(onlineFlowPermData.permUrlSet); + String sessionId = tokenData.getSessionId(); + // 缓存用户的权限资源,这里缓存的是基于URL验证的权限资源,比如在线表单、工作流和数据表中的白名单资源。 + this.putUserSysPermCache(sessionId, permSet); + // 缓存权限字字段,StpInterfaceImpl中会从缓存中读取,并交给satoken进行接口权限的验证。 + this.putUserSysPermCodeCache(sessionId, permCodeList); + sysDataPermService.putDataPermCache(sessionId, user.getUserId(), user.getDeptId()); + } + this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList); + return jsonData; + } + + private TokenData loginAndCreateToken(SysUser user) { + String deviceType = MyCommonUtil.getDeviceTypeWithString(); + LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class); + String loginId = SaTokenUtil.makeLoginId(userInfo); + StpUtil.login(loginId, deviceType); + SaSession session = StpUtil.getTokenSession(); + TokenData tokenData = this.buildTokenData(user, session.getId(), StpUtil.getLoginDevice()); + String mySessionId = RedisKeyUtil.getSessionIdPrefix(tokenData, user.getLoginName()) + MyCommonUtil.generateUuid(); + tokenData.setMySessionId(mySessionId); + tokenData.setToken(session.getToken()); + redissonClient.getBucket(mySessionId) + .set(JSON.toJSONString(tokenData), appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + session.set(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData); + return tokenData; + } + + private JSONObject createResponseData(SysUser user) { + JSONObject jsonData = new JSONObject(); + jsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, StpUtil.getTokenValue()); + jsonData.put(SHOW_NAME_FIELD, user.getShowName()); + jsonData.put(IS_ADMIN, user.getUserType() == SysUserType.TYPE_ADMIN); + if (user.getDeptId() != null) { + SysDept dept = sysDeptService.getById(user.getDeptId()); + jsonData.put("deptName", dept.getDeptName()); + } + if (StrUtil.isNotBlank(user.getHeadImageUrl())) { + jsonData.put(HEAD_IMAGE_URL_FIELD, user.getHeadImageUrl()); + } + return jsonData; + } + + private void appendResponseMenuAndPermCodeData( + JSONObject responseData, Collection allMenuList, Collection menuCodeList) { + allMenuList.stream() + .filter(m -> m.getExtraObject() != null && StrUtil.isNotBlank(m.getExtraObject().getMenuCode())) + .forEach(m -> CollUtil.addAll(menuCodeList, m.getExtraObject().getMenuCode())); + List menuList = allMenuList.stream() + .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList()); + responseData.put("menuList", menuList); + responseData.put("permCodeList", menuCodeList); + } + + private TokenData buildTokenData(SysUser user, String sessionId, String deviceType) { + TokenData tokenData = new TokenData(); + tokenData.setSessionId(sessionId); + tokenData.setUserId(user.getUserId()); + tokenData.setDeptId(user.getDeptId()); + tokenData.setLoginName(user.getLoginName()); + tokenData.setShowName(user.getShowName()); + tokenData.setIsAdmin(user.getUserType().equals(SysUserType.TYPE_ADMIN)); + tokenData.setLoginIp(IpUtil.getRemoteIpAddress(ContextUtil.getHttpRequest())); + tokenData.setLoginTime(new Date()); + tokenData.setDeviceType(deviceType); + tokenData.setHeadImageUrl(user.getHeadImageUrl()); + List userPostList = sysPostService.getSysUserPostListByUserId(user.getUserId()); + if (CollUtil.isNotEmpty(userPostList)) { + Set deptPostIdSet = userPostList.stream().map(SysUserPost::getDeptPostId).collect(Collectors.toSet()); + tokenData.setDeptPostIds(StrUtil.join(",", deptPostIdSet)); + Set postIdSet = userPostList.stream().map(SysUserPost::getPostId).collect(Collectors.toSet()); + tokenData.setPostIds(StrUtil.join(",", postIdSet)); + } + List userRoleList = sysRoleService.getSysUserRoleListByUserId(user.getUserId()); + if (CollUtil.isNotEmpty(userRoleList)) { + Set userRoleIdSet = userRoleList.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet()); + tokenData.setRoleIds(StrUtil.join(",", userRoleIdSet)); + } + return tokenData; + } + + private void putUserSysPermCache(String sessionId, Collection permUrlSet) { + if (CollUtil.isEmpty(permUrlSet)) { + return; + } + String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId); + RSet redisPermSet = redissonClient.getSet(sessionPermKey); + redisPermSet.addAll(permUrlSet); + redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + } + + private void putUserSysPermCodeCache(String sessionId, Collection permCodeSet) { + if (CollUtil.isEmpty(permCodeSet)) { + return; + } + String sessionPermCodeKey = RedisKeyUtil.makeSessionPermCodeKey(sessionId); + RSet redisPermSet = redissonClient.getSet(sessionPermCodeKey); + redisPermSet.addAll(permCodeSet); + redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + } + + private OnlinePermData getOnlineMenuPermData(Collection allMenuList) { + List onlineMenuList = allMenuList.stream() + .filter(m -> m.getOnlineFormId() != null && m.getMenuType().equals(SysMenuType.TYPE_BUTTON)) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(onlineMenuList)) { + return new OnlinePermData(); + } + Set formIds = allMenuList.stream() + .filter(m -> m.getOnlineFormId() != null + && m.getOnlineFlowEntryId() == null + && m.getMenuType().equals(SysMenuType.TYPE_MENU)) + .map(SysMenu::getOnlineFormId) + .collect(Collectors.toSet()); + Set viewFormIds = onlineMenuList.stream() + .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_VIEW) + .map(SysMenu::getOnlineFormId) + .collect(Collectors.toSet()); + Set editFormIds = onlineMenuList.stream() + .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_EDIT) + .map(SysMenu::getOnlineFormId) + .collect(Collectors.toSet()); + Map permDataMap = + onlineOperationService.calculatePermData(formIds, viewFormIds, editFormIds); + OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null); + permData.permUrlSet.addAll(permData.onlineWhitelistUrls); + return permData; + } + + private OnlinePermData getFlowOnlineMenuPermData(Collection allMenuList) { + List flowOnlineMenuList = allMenuList.stream() + .filter(m -> m.getOnlineFlowEntryId() != null).collect(Collectors.toList()); + Set entryIds = flowOnlineMenuList.stream() + .map(SysMenu::getOnlineFlowEntryId).collect(Collectors.toSet()); + List> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds); + List flowOnlinePermDataList = + MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class); + OnlinePermData permData = new OnlinePermData(); + flowOnlinePermDataList.forEach(perm -> { + permData.permCodeSet.addAll(perm.getPermCodeList()); + permData.permUrlSet.addAll(perm.getPermList()); + }); + return permData; + } + + static class OnlinePermData { + public final Set permCodeSet = new HashSet<>(); + public final Set permUrlSet = new HashSet<>(); + public final List onlineWhitelistUrls = new LinkedList<>(); + } + + @Data + static class OnlineFlowPermData { + private List permCodeList; + private List permList; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java new file mode 100644 index 00000000..6e57c15d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java @@ -0,0 +1,89 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.RedisKeyUtil; +import io.swagger.v3.oas.annotations.tags.Tag; +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.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +/** + * 在线用户控制器对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线用户接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/loginUser") +public class LoginUserController { + + @Autowired + private RedissonClient redissonClient; + + /** + * 显示在线用户列表。 + * + * @param loginName 登录名过滤。 + * @param pageParam 分页参数。 + * @return 登录用户信息列表。 + */ + @SaCheckPermission("loginUser.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody String loginName, @MyRequestBody MyPageParam pageParam) { + int skipCount = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + String patternKey; + if (StrUtil.isBlank(loginName)) { + patternKey = RedisKeyUtil.getSessionIdPrefix() + "*"; + } else { + patternKey = RedisKeyUtil.getSessionIdPrefix(loginName) + "*"; + } + List loginUserInfoList = new LinkedList<>(); + Iterable keys = redissonClient.getKeys().getKeysByPattern(patternKey); + for (String key : keys) { + loginUserInfoList.add(this.buildTokenDataByRedisKey(key)); + } + loginUserInfoList.sort((o1, o2) -> (int) (o2.getLoginTime().getTime() - o1.getLoginTime().getTime())); + int toIndex = Math.min(skipCount + pageParam.getPageSize(), loginUserInfoList.size()); + List resultList = loginUserInfoList.subList(skipCount, toIndex); + return ResponseResult.success(new MyPageData<>(resultList, (long) loginUserInfoList.size())); + } + + /** + * 强制下线指定登录会话。 + * + * @param sessionId 待强制下线的SessionId。 + * @return 应答结果对象。 + */ + @SaCheckPermission("loginUser.delete") + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody String sessionId) { + RBucket sessionData = redissonClient.getBucket(sessionId); + TokenData tokenData = JSON.parseObject(sessionData.get(), TokenData.class); + StpUtil.kickoutByTokenValue(tokenData.getToken()); + sessionData.delete(); + return ResponseResult.success(); + } + + private LoginUserInfo buildTokenDataByRedisKey(String key) { + RBucket sessionData = redissonClient.getBucket(key); + TokenData tokenData = JSON.parseObject(sessionData.get(), TokenData.class); + LoginUserInfo userInfo = BeanUtil.copyProperties(tokenData, LoginUserInfo.class); + userInfo.setSessionId(tokenData.getMySessionId()); + return userInfo; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java new file mode 100644 index 00000000..e389b0e7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java @@ -0,0 +1,337 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.alibaba.fastjson.TypeReference; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.upms.dto.SysDataPermDto; +import com.orangeforms.webadmin.upms.dto.SysUserDto; +import com.orangeforms.webadmin.upms.vo.SysDataPermVo; +import com.orangeforms.webadmin.upms.vo.SysUserVo; +import com.orangeforms.webadmin.upms.model.SysDataPerm; +import com.orangeforms.webadmin.upms.model.SysUser; +import com.orangeforms.webadmin.upms.service.SysDataPermService; +import com.orangeforms.webadmin.upms.service.SysUserService; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 数据权限接口控制器对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "数据权限管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysDataPerm") +public class SysDataPermController { + + @Autowired + private SysDataPermService sysDataPermService; + @Autowired + private SysUserService sysUserService; + + /** + * 添加新数据权限操作。 + * + * @param sysDataPermDto 新增对象。 + * @param deptIdListString 数据权限关联的部门Id列表,多个之间逗号分隔。 + * @param menuIdListString 数据权限关联的菜单Id列表,多个之间逗号分隔。 + * @return 应答结果对象。包含新增数据权限对象的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysDataPermDto.dataPermId", + "sysDataPermDto.createTimeStart", + "sysDataPermDto.createTimeEnd", + "sysDataPermDto.searchString"}) + @SaCheckPermission("sysDataPerm.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody SysDataPermDto sysDataPermDto, + @MyRequestBody String deptIdListString, + @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDataPermDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDataPerm sysDataPerm = MyModelUtil.copyTo(sysDataPermDto, SysDataPerm.class); + CallResult result = sysDataPermService.verifyRelatedData(sysDataPerm, deptIdListString, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + Set deptIdSet = null; + if (result.getData() != null) { + deptIdSet = result.getData().getObject("deptIdSet", new TypeReference>(){}); + } + sysDataPermService.saveNew(sysDataPerm, deptIdSet, menuIdSet); + return ResponseResult.success(sysDataPerm.getDataPermId()); + } + + /** + * 更新数据权限操作。 + * + * @param sysDataPermDto 更新的数据权限对象。 + * @param deptIdListString 数据权限关联的部门Id列表,多个之间逗号分隔。 + * @param menuIdListString 数据权限关联的菜单Id列表,多个之间逗号分隔。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysDataPermDto.createTimeStart", + "sysDataPermDto.createTimeEnd", + "sysDataPermDto.searchString"}) + @SaCheckPermission("sysDataPerm.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update( + @MyRequestBody SysDataPermDto sysDataPermDto, + @MyRequestBody String deptIdListString, + @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDataPermDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDataPerm originalSysDataPerm = sysDataPermService.getById(sysDataPermDto.getDataPermId()); + if (originalSysDataPerm == null) { + errorMessage = "数据验证失败,当前数据权限并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + SysDataPerm sysDataPerm = MyModelUtil.copyTo(sysDataPermDto, SysDataPerm.class); + CallResult result = sysDataPermService.verifyRelatedData(sysDataPerm, deptIdListString, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set deptIdSet = null; + if (result.getData() != null) { + deptIdSet = result.getData().getObject("deptIdSet", new TypeReference>(){}); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + if (!sysDataPermService.update(sysDataPerm, originalSysDataPerm, deptIdSet, menuIdSet)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除数据权限操作。 + * + * @param dataPermId 待删除数据权限主键Id。 + * @return 应答数据结果。 + */ + @SaCheckPermission("sysDataPerm.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long dataPermId) { + if (MyCommonUtil.existBlankArgument(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDataPermService.remove(dataPermId)) { + String errorMessage = "数据操作失败,数据权限不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 查看数据权限列表。 + * + * @param sysDataPermDtoFilter 数据权限查询过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象。包含数据权限列表。 + */ + @SaCheckPermission("sysDataPerm.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysDataPermDto sysDataPermDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysDataPerm filter = MyModelUtil.copyTo(sysDataPermDtoFilter, SysDataPerm.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysDataPerm.class); + List dataPermList = sysDataPermService.getSysDataPermListWithRelation(filter, orderBy); + List dataPermVoList = MyModelUtil.copyCollectionTo(dataPermList, SysDataPermVo.class); + long totalCount = 0L; + if (dataPermList instanceof Page) { + totalCount = ((Page) dataPermList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(dataPermVoList, totalCount)); + } + + /** + * 查看单条数据权限详情。 + * + * @param dataPermId 数据权限的主键Id。 + * @return 应答结果对象,包含数据权限的详情。 + */ + @SaCheckPermission("sysDataPerm.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long dataPermId) { + if (MyCommonUtil.existBlankArgument(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysDataPerm dataPerm = sysDataPermService.getByIdWithRelation(dataPermId, MyRelationParam.full()); + if (dataPerm == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysDataPermVo dataPermVo = MyModelUtil.copyTo(dataPerm, SysDataPermVo.class); + return ResponseResult.success(dataPermVo); + } + + /** + * 拥有指定数据权限的用户列表。 + * + * @param dataPermId 数据权限Id。 + * @param sysUserDtoFilter 用户过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysDataPerm.view") + @PostMapping("/listDataPermUser") + public ResponseResult> listDataPermUser( + @MyRequestBody Long dataPermId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doDataPermUserVerify(dataPermId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = sysUserService.getSysUserListByDataPermId(dataPermId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 获取不包含指定数据权限Id的用户列表。 + * 用户和数据权限是多对多关系,当前接口将返回没有赋值指定DataPermId的用户列表。可用于给数据权限添加新用户。 + * + * @param dataPermId 数据权限主键Id。 + * @param sysUserDtoFilter 用户数据的过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysDataPerm.update") + @PostMapping("/listNotInDataPermUser") + public ResponseResult> listNotInDataPermUser( + @MyRequestBody Long dataPermId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doDataPermUserVerify(dataPermId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = + sysUserService.getNotInSysUserListByDataPermId(dataPermId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 为指定数据权限添加用户列表。该操作可同时给一批用户赋值数据权限,并在同一事务内完成。 + * + * @param dataPermId 数据权限主键Id。 + * @param userIdListString 逗号分隔的用户Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDataPerm.update") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addDataPermUser") + public ResponseResult addDataPermUser( + @MyRequestBody Long dataPermId, @MyRequestBody String userIdListString) { + if (MyCommonUtil.existBlankArgument(dataPermId, userIdListString)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + Set userIdSet = + Arrays.stream(userIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysDataPermService.existId(dataPermId) + || !sysUserService.existUniqueKeyList("userId", userIdSet)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + sysDataPermService.addDataPermUserList(dataPermId, userIdSet); + return ResponseResult.success(); + } + + /** + * 为指定用户移除指定数据权限。 + * + * @param dataPermId 指定数据权限主键Id。 + * @param userId 指定用户主键Id。 + * @return 应答数据结果。 + */ + @SaCheckPermission("sysDataPerm.update") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteDataPermUser") + public ResponseResult deleteDataPermUser( + @MyRequestBody Long dataPermId, @MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(dataPermId, userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDataPermService.removeDataPermUser(dataPermId, userId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 以字典形式返回全部数据权限管理数据集合。字典的键值为[dataPermId, dataPermName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysDataPermDto filter) { + List resultList = + sysDataPermService.getListByFilter(MyModelUtil.copyTo(filter, SysDataPerm.class)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysDataPerm::getDataPermId, SysDataPerm::getDataPermName)); + } + + private ResponseResult doDataPermUserVerify(Long dataPermId) { + if (MyCommonUtil.existBlankArgument(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDataPermService.existId(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java new file mode 100644 index 00000000..3c1fb0f8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java @@ -0,0 +1,428 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ObjectUtil; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.vo.*; +import com.orangeforms.webadmin.upms.dto.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 部门管理操作控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "部门管理管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysDept") +public class SysDeptController { + + @Autowired + private SysPostService sysPostService; + @Autowired + private SysDeptService sysDeptService; + + /** + * 新增部门管理数据。 + * + * @param sysDeptDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysDeptDto.deptId"}) + @SaCheckPermission("sysDept.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody SysDeptDto sysDeptDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDept sysDept = MyModelUtil.copyTo(sysDeptDto, SysDept.class); + // 验证父Id的数据合法性 + SysDept parentSysDept = null; + if (MyCommonUtil.isNotBlankOrNull(sysDept.getParentId())) { + parentSysDept = sysDeptService.getById(sysDept.getParentId()); + if (parentSysDept == null) { + errorMessage = "数据验证失败,关联的父节点并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_PARENT_ID_NOT_EXIST, errorMessage); + } + } + sysDept = sysDeptService.saveNew(sysDept, parentSysDept); + return ResponseResult.success(sysDept.getDeptId()); + } + + /** + * 更新部门管理数据。 + * + * @param sysDeptDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody SysDeptDto sysDeptDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDept sysDept = MyModelUtil.copyTo(sysDeptDto, SysDept.class); + SysDept originalSysDept = sysDeptService.getById(sysDept.getDeptId()); + if (originalSysDept == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + // 验证父Id的数据合法性 + if (MyCommonUtil.isNotBlankOrNull(sysDept.getParentId()) + && ObjectUtil.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) { + SysDept parentSysDept = sysDeptService.getById(sysDept.getParentId()); + if (parentSysDept == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,关联的 [父节点] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_PARENT_ID_NOT_EXIST, errorMessage); + } + } + if (!sysDeptService.update(sysDept, originalSysDept)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除部门管理数据。 + * + * @param deptId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long deptId) { + if (MyCommonUtil.existBlankArgument(deptId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + return this.doDelete(deptId); + } + + /** + * 批量删除部门管理数据。 + * + * @param deptIdList 待删除对象的主键Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.delete") + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatch") + public ResponseResult deleteBatch(@MyRequestBody List deptIdList) { + if (MyCommonUtil.existBlankArgument(deptIdList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + for (Long deptId : deptIdList) { + ResponseResult responseResult = this.doDelete(deptId); + if (!responseResult.isSuccess()) { + return responseResult; + } + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的部门管理列表。 + * + * @param sysDeptDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("sysDept.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysDeptDto sysDeptDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + SysDept sysDeptFilter = MyModelUtil.copyTo(sysDeptDtoFilter, SysDept.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysDept.class); + List sysDeptList = sysDeptService.getSysDeptListWithRelation(sysDeptFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysDeptList, SysDeptVo.class)); + } + + /** + * 查看指定部门管理对象详情。 + * + * @param deptId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("sysDept.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long deptId) { + SysDept sysDept = sysDeptService.getByIdWithRelation(deptId, MyRelationParam.full()); + if (sysDept == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysDeptVo sysDeptVo = MyModelUtil.copyTo(sysDept, SysDeptVo.class); + return ResponseResult.success(sysDeptVo); + } + + /** + * 列出不与指定部门管理存在多对多关系的 [岗位管理] 列表数据。通常用于查看添加新 [岗位管理] 对象的候选列表。 + * + * @param deptId 主表关联字段。 + * @param sysPostDtoFilter [岗位管理] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/listNotInSysDeptPost") + public ResponseResult> listNotInSysDeptPost( + @MyRequestBody Long deptId, + @MyRequestBody SysPostDto sysPostDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (MyCommonUtil.isNotBlankOrNull(deptId) && !sysDeptService.existId(deptId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysPost filter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class); + List sysPostList; + if (MyCommonUtil.isNotBlankOrNull(deptId)) { + sysPostList = sysPostService.getNotInSysPostListByDeptId(deptId, filter, orderBy); + } else { + sysPostList = sysPostService.getSysPostList(filter, orderBy); + sysPostService.buildRelationForDataList(sysPostList, MyRelationParam.dictOnly()); + } + return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class)); + } + + /** + * 列出与指定部门管理存在多对多关系的 [岗位管理] 列表数据。 + * + * @param deptId 主表关联字段。 + * @param sysPostDtoFilter [岗位管理] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("sysDept.view") + @PostMapping("/listSysDeptPost") + public ResponseResult> listSysDeptPost( + @MyRequestBody(required = true) Long deptId, + @MyRequestBody SysPostDto sysPostDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (!sysDeptService.existId(deptId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysPost filter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class); + List sysPostList = sysPostService.getSysPostListByDeptId(deptId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class)); + } + + /** + * 批量添加部门管理和 [岗位管理] 对象的多对多关联关系数据。 + * + * @param deptId 主表主键Id。 + * @param sysDeptPostDtoList 关联对象列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/addSysDeptPost") + public ResponseResult addSysDeptPost( + @MyRequestBody Long deptId, + @MyRequestBody List sysDeptPostDtoList) { + if (MyCommonUtil.existBlankArgument(deptId, sysDeptPostDtoList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptPostDtoList); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Set postIdSet = sysDeptPostDtoList.stream().map(SysDeptPostDto::getPostId).collect(Collectors.toSet()); + if (!sysDeptService.existId(deptId) || !sysPostService.existUniqueKeyList("postId", postIdSet)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + List sysDeptPostList = MyModelUtil.copyCollectionTo(sysDeptPostDtoList, SysDeptPost.class); + sysDeptService.addSysDeptPostList(sysDeptPostList, deptId); + return ResponseResult.success(); + } + + /** + * 更新指定部门管理和指定 [岗位管理] 的多对多关联数据。 + * + * @param sysDeptPostDto 对多对中间表对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/updateSysDeptPost") + public ResponseResult updateSysDeptPost(@MyRequestBody SysDeptPostDto sysDeptPostDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptPostDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDeptPost sysDeptPost = MyModelUtil.copyTo(sysDeptPostDto, SysDeptPost.class); + if (!sysDeptService.updateSysDeptPost(sysDeptPost)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 显示部门管理和指定 [岗位管理] 的多对多关联详情数据。 + * + * @param deptId 主表主键Id。 + * @param postId 从表主键Id。 + * @return 应答结果对象,包括中间表详情。 + */ + @SaCheckPermission("sysDept.update") + @GetMapping("/viewSysDeptPost") + public ResponseResult viewSysDeptPost(@RequestParam Long deptId, @RequestParam Long postId) { + SysDeptPost sysDeptPost = sysDeptService.getSysDeptPost(deptId, postId); + if (sysDeptPost == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysDeptPostVo sysDeptPostVo = MyModelUtil.copyTo(sysDeptPost, SysDeptPostVo.class); + return ResponseResult.success(sysDeptPostVo); + } + + /** + * 移除指定部门管理和指定 [岗位管理] 的多对多关联关系。 + * + * @param deptId 主表主键Id。 + * @param postId 从表主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/deleteSysDeptPost") + public ResponseResult deleteSysDeptPost(@MyRequestBody Long deptId, @MyRequestBody Long postId) { + if (MyCommonUtil.existBlankArgument(deptId, postId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDeptService.removeSysDeptPost(deptId, postId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 获取部门岗位多对多关联数据,及其关联的部门和岗位数据。 + * + * @param deptId 部门Id,如果为空,返回全部数据列表。 + * @return 部门岗位多对多关联数据,及其关联的部门和岗位数据 + */ + @GetMapping("/listSysDeptPostWithRelation") + public ResponseResult>> listSysDeptPostWithRelation( + @RequestParam(required = false) Long deptId) { + return ResponseResult.success(sysDeptService.getSysDeptPostListWithRelationByDeptId(deptId)); + } + + /** + * 以字典形式返回全部部门管理数据集合。字典的键值为[deptId, deptName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysDeptDto filter) { + List resultList = + sysDeptService.getListByFilter(MyModelUtil.copyTo(filter, SysDept.class)); + return ResponseResult.success(MyCommonUtil.toDictDataList( + resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = sysDeptService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success(MyCommonUtil.toDictDataList( + resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId)); + } + + /** + * 根据父主键Id,以字典的形式返回其下级数据列表。 + * 白名单接口,登录用户均可访问。 + * + * @param parentId 父主键Id。 + * @return 按照字典的形式返回下级数据列表。 + */ + @GetMapping("/listDictByParentId") + public ResponseResult>> listDictByParentId(@RequestParam(required = false) Long parentId) { + List resultList = sysDeptService.getListByParentId("parentId", parentId); + return ResponseResult.success(MyCommonUtil.toDictDataList( + resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId)); + } + + /** + * 根据父主键Id列表,获取当前部门Id及其所有下级部门Id列表。 + * 白名单接口,登录用户均可访问。 + * + * @param parentIds 父主键Id列表,多个Id之间逗号分隔。 + * @return 获取当前部门Id及其所有下级部门Id列表。 + */ + @GetMapping("/listAllChildDeptIdByParentIds") + public ResponseResult> listAllChildDeptIdByParentIds( + @RequestParam(required = false) String parentIds) { + List parentIdList = StrUtil.split(parentIds, ',') + .stream().map(Long::valueOf).collect(Collectors.toList()); + return ResponseResult.success(sysDeptService.getAllChildDeptIdByParentIds(parentIdList)); + } + + private ResponseResult doDelete(Long deptId) { + String errorMessage; + // 验证关联Id的数据合法性 + SysDept originalSysDept = sysDeptService.getById(deptId); + if (originalSysDept == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (sysDeptService.hasChildren(deptId)) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象存在子对象] ,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage); + } + if (sysDeptService.hasChildrenUser(deptId)) { + errorMessage = "数据验证失败,请先移除部门用户数据后,再删除当前部门!"; + return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage); + } + if (!sysDeptService.remove(deptId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java new file mode 100644 index 00000000..0ea5f339 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java @@ -0,0 +1,231 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.upms.dto.SysMenuDto; +import com.orangeforms.webadmin.upms.vo.SysMenuVo; +import com.orangeforms.webadmin.upms.model.SysMenu; +import com.orangeforms.webadmin.upms.model.SysDataPerm; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import com.orangeforms.webadmin.upms.service.SysMenuService; +import com.orangeforms.webadmin.upms.service.SysDataPermService; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 菜单管理接口控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "菜单管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysMenu") +public class SysMenuController { + + @Autowired + private SysMenuService sysMenuService; + @Autowired + private SysDataPermService sysDataPermService; + + /** + * 添加新菜单操作。 + * + * @param sysMenuDto 新菜单对象。 + * @return 应答结果对象,包含新增菜单的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysMenuDto.menuId"}) + @SaCheckPermission("sysMenu.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody SysMenuDto sysMenuDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysMenuDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysMenu sysMenu = MyModelUtil.copyTo(sysMenuDto, SysMenu.class); + if (sysMenu.getParentId() != null) { + SysMenu parentSysMenu = sysMenuService.getById(sysMenu.getParentId()); + if (parentSysMenu == null) { + errorMessage = "数据验证失败,关联的父菜单不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (parentSysMenu.getOnlineFormId() != null) { + errorMessage = "数据验证失败,不能为动态表单菜单添加子菜单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + CallResult result = sysMenuService.verifyRelatedData(sysMenu, null); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + sysMenuService.saveNew(sysMenu); + return ResponseResult.success(sysMenu.getMenuId()); + } + + /** + * 更新菜单数据操作。 + * + * @param sysMenuDto 新菜单对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysMenu.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody SysMenuDto sysMenuDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysMenuDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysMenu originalSysMenu = sysMenuService.getById(sysMenuDto.getMenuId()); + if (originalSysMenu == null) { + errorMessage = "数据验证失败,当前菜单并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + SysMenu sysMenu = MyModelUtil.copyTo(sysMenuDto, SysMenu.class); + if (ObjectUtil.notEqual(originalSysMenu.getOnlineFormId(), sysMenu.getOnlineFormId())) { + if (originalSysMenu.getOnlineFormId() == null) { + errorMessage = "数据验证失败,不能为当前菜单添加在线表单Id属性!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (sysMenu.getOnlineFormId() == null) { + errorMessage = "数据验证失败,不能去掉当前菜单的在线表单Id属性!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + if (originalSysMenu.getOnlineFormId() != null + && originalSysMenu.getMenuType().equals(SysMenuType.TYPE_BUTTON)) { + errorMessage = "数据验证失败,在线表单的内置菜单不能编辑!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult result = sysMenuService.verifyRelatedData(sysMenu, originalSysMenu); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + if (!sysMenuService.update(sysMenu, originalSysMenu)) { + errorMessage = "数据验证失败,当前权限字并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除指定菜单操作。 + * + * @param menuId 指定菜单主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysMenu.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long menuId) { + if (MyCommonUtil.existBlankArgument(menuId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + String errorMessage; + SysMenu menu = sysMenuService.getById(menuId); + if (menu == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (menu.getOnlineFormId() != null && menu.getMenuType().equals(SysMenuType.TYPE_BUTTON)) { + errorMessage = "数据验证失败,在线表单的内置菜单不能删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 对于在线表单,无需进行子菜单的验证,而是在删除的时候,连同子菜单一起删除。 + if (menu.getOnlineFormId() == null && sysMenuService.hasChildren(menuId)) { + errorMessage = "数据验证失败,当前菜单存在下级菜单!"; + return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage); + } + List dataPermList = sysDataPermService.getSysDataPermListByMenuId(menuId); + if (CollUtil.isNotEmpty(dataPermList)) { + SysDataPerm dataPerm = dataPermList.get(0); + errorMessage = "数据验证失败,当前菜单正在被数据权限 [" + dataPerm.getDataPermName() + "] 引用,不能直接删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!sysMenuService.remove(menu)) { + errorMessage = "数据操作失败,菜单不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 获取全部菜单列表。 + * + * @return 应答结果对象,包含全部菜单数据列表。 + */ + @SaCheckPermission("sysMenu.view") + @PostMapping("/list") + public ResponseResult> list() { + List resultList = this.getAllMenuListByShowOrder(); + return ResponseResult.success(MyModelUtil.copyCollectionTo(resultList, SysMenuVo.class)); + } + + /** + * 查看指定菜单数据详情。 + * + * @param menuId 指定菜单主键Id。 + * @return 应答结果对象,包含菜单详情。 + */ + @SaCheckPermission("sysMenu.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long menuId) { + if (MyCommonUtil.existBlankArgument(menuId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysMenu sysMenu = sysMenuService.getByIdWithRelation(menuId, MyRelationParam.full()); + if (sysMenu == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysMenuVo sysMenuVo = MyModelUtil.copyTo(sysMenu, SysMenuVo.class); + return ResponseResult.success(sysMenuVo); + } + + /** + * 以字典形式返回目录和菜单类型的菜单管理数据集合。字典的键值为[menuId, menuName]。 + * 白名单接口,登录用户均可访问。 + * + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listMenuDict") + public ResponseResult>> listMenuDict() { + List resultList = this.getAllMenuListByShowOrder(); + resultList = resultList.stream() + .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList()); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId)); + } + + /** + * 以字典形式返回全部的菜单管理数据集合。字典的键值为[menuId, menuName]。 + * 白名单接口,登录用户均可访问。 + * + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict() { + List resultList = this.getAllMenuListByShowOrder(); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId)); + } + + private List getAllMenuListByShowOrder() { + return sysMenuService.getAllListByOrder("showOrder"); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java new file mode 100644 index 00000000..d7ec940f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java @@ -0,0 +1,63 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.log.model.SysOperationLog; +import com.orangeforms.common.log.service.SysOperationLogService; +import com.orangeforms.common.log.dto.SysOperationLogDto; +import com.orangeforms.common.log.vo.SysOperationLogVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 操作日志接口控制器对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "操作日志接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysOperationLog") +public class SysOperationLogController { + + @Autowired + private SysOperationLogService operationLogService; + + /** + * 数据权限列表。 + * + * @param sysOperationLogDtoFilter 操作日志查询过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象。包含操作日志列表。 + */ + @SaCheckPermission("sysOperationLog.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysOperationLogDto sysOperationLogDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysOperationLog filter = MyModelUtil.copyTo(sysOperationLogDtoFilter, SysOperationLog.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysOperationLog.class); + List operationLogList = operationLogService.getSysOperationLogList(filter, orderBy); + List operationLogVoList = MyModelUtil.copyCollectionTo(operationLogList, SysOperationLogVo.class); + long totalCount = 0L; + if (operationLogList instanceof Page) { + totalCount = ((Page) operationLogList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(operationLogVoList, totalCount)); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java new file mode 100644 index 00000000..9f4dcec4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java @@ -0,0 +1,183 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.webadmin.upms.dto.SysPostDto; +import com.orangeforms.webadmin.upms.model.SysPost; +import com.orangeforms.webadmin.upms.service.SysPostService; +import com.orangeforms.webadmin.upms.vo.SysPostVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import jakarta.validation.groups.Default; + +/** + * 岗位管理操作控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "岗位管理操作管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysPost") +public class SysPostController { + + @Autowired + private SysPostService sysPostService; + + /** + * 新增岗位管理数据。 + * + * @param sysPostDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysPostDto.postId"}) + @SaCheckPermission("sysPost.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody SysPostDto sysPostDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysPostDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysPost sysPost = MyModelUtil.copyTo(sysPostDto, SysPost.class); + sysPost = sysPostService.saveNew(sysPost); + return ResponseResult.success(sysPost.getPostId()); + } + + /** + * 更新岗位管理数据。 + * + * @param sysPostDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysPost.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody SysPostDto sysPostDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysPostDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysPost sysPost = MyModelUtil.copyTo(sysPostDto, SysPost.class); + SysPost originalSysPost = sysPostService.getById(sysPost.getPostId()); + if (originalSysPost == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!sysPostService.update(sysPost, originalSysPost)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除岗位管理数据。 + * + * @param postId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysPost.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long postId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(postId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + SysPost originalSysPost = sysPostService.getById(postId); + if (originalSysPost == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!sysPostService.remove(postId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的岗位管理列表。 + * + * @param sysPostDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("sysPost.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysPostDto sysPostDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysPost sysPostFilter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class); + List sysPostList = sysPostService.getSysPostListWithRelation(sysPostFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class)); + } + + /** + * 查看指定岗位管理对象详情。 + * + * @param postId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("sysPost.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long postId) { + if (MyCommonUtil.existBlankArgument(postId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysPost sysPost = sysPostService.getByIdWithRelation(postId, MyRelationParam.full()); + if (sysPost == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysPostVo sysPostVo = MyModelUtil.copyTo(sysPost, SysPostVo.class); + return ResponseResult.success(sysPostVo); + } + + /** + * 以字典形式返回全部岗位管理数据集合。字典的键值为[postId, postName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysPostDto filter) { + List resultList = sysPostService.getListByFilter(MyModelUtil.copyTo(filter, SysPost.class)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysPost::getPostId, SysPost::getPostName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param postIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List postIds) { + List resultList = sysPostService.getInList(new HashSet<>(postIds)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysPost::getPostId, SysPost::getPostName)); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java new file mode 100644 index 00000000..25e5c51f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java @@ -0,0 +1,331 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.alibaba.fastjson.TypeReference; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.upms.dto.SysRoleDto; +import com.orangeforms.webadmin.upms.dto.SysUserDto; +import com.orangeforms.webadmin.upms.vo.SysRoleVo; +import com.orangeforms.webadmin.upms.vo.SysUserVo; +import com.orangeforms.webadmin.upms.model.SysRole; +import com.orangeforms.webadmin.upms.model.SysUser; +import com.orangeforms.webadmin.upms.model.SysUserRole; +import com.orangeforms.webadmin.upms.service.SysRoleService; +import com.orangeforms.webadmin.upms.service.SysUserService; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 角色管理接口控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "角色管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysRole") +public class SysRoleController { + + @Autowired + private SysRoleService sysRoleService; + @Autowired + private SysUserService sysUserService; + + /** + * 新增角色操作。 + * + * @param sysRoleDto 新增角色对象。 + * @param menuIdListString 与当前角色Id绑定的menuId列表,多个menuId之间逗号分隔。 + * @return 应答结果对象,包含新增角色的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysRoleDto.roleId", "sysRoleDto.createTimeStart", "sysRoleDto.createTimeEnd"}) + @SaCheckPermission("sysRole.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody SysRoleDto sysRoleDto, @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysRoleDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysRole sysRole = MyModelUtil.copyTo(sysRoleDto, SysRole.class); + CallResult result = sysRoleService.verifyRelatedData(sysRole, null, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + sysRoleService.saveNew(sysRole, menuIdSet); + return ResponseResult.success(sysRole.getRoleId()); + } + + /** + * 更新角色操作。 + * + * @param sysRoleDto 更新角色对象。 + * @param menuIdListString 与当前角色Id绑定的menuId列表,多个menuId之间逗号分隔。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = {"sysRoleDto.createTimeStart", "sysRoleDto.createTimeEnd"}) + @SaCheckPermission("sysRole.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update( + @MyRequestBody SysRoleDto sysRoleDto, @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysRoleDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysRole originalSysRole = sysRoleService.getById(sysRoleDto.getRoleId()); + if (originalSysRole == null) { + errorMessage = "数据验证失败,当前角色并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + SysRole sysRole = MyModelUtil.copyTo(sysRoleDto, SysRole.class); + CallResult result = sysRoleService.verifyRelatedData(sysRole, originalSysRole, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + if (!sysRoleService.update(sysRole, originalSysRole, menuIdSet)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除指定角色操作。 + * + * @param roleId 指定角色主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysRole.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long roleId) { + if (MyCommonUtil.existBlankArgument(roleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysRoleService.remove(roleId)) { + String errorMessage = "数据操作失败,角色不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 查看角色列表。 + * + * @param sysRoleDtoFilter 角色过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含角色列表。 + */ + @SaCheckPermission("sysRole.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysRoleDto sysRoleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysRole filter = MyModelUtil.copyTo(sysRoleDtoFilter, SysRole.class); + List roleList = sysRoleService.getSysRoleList( + filter, MyOrderParam.buildOrderBy(orderParam, SysRole.class)); + List roleVoList = MyModelUtil.copyCollectionTo(roleList, SysRoleVo.class); + long totalCount = 0L; + if (roleList instanceof Page) { + totalCount = ((Page) roleList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(roleVoList, totalCount)); + } + + /** + * 查看角色详情。 + * + * @param roleId 指定角色主键Id。 + * @return 应答结果对象,包含角色详情对象。 + */ + @SaCheckPermission("sysRole.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long roleId) { + if (MyCommonUtil.existBlankArgument(roleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysRole sysRole = sysRoleService.getByIdWithRelation(roleId, MyRelationParam.full()); + if (sysRole == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysRoleVo sysRoleVo = MyModelUtil.copyTo(sysRole, SysRoleVo.class); + return ResponseResult.success(sysRoleVo); + } + + /** + * 拥有指定角色的用户列表。 + * + * @param roleId 角色主键Id。 + * @param sysUserDtoFilter 用户过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysRole.view") + @PostMapping("/listUserRole") + public ResponseResult> listUserRole( + @MyRequestBody Long roleId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doRoleUserVerify(roleId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = sysUserService.getSysUserListByRoleId(roleId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 获取不包含指定角色Id的用户列表。 + * 用户和角色是多对多关系,当前接口将返回没有赋值指定RoleId的用户列表。可用于给角色添加新用户。 + * + * @param roleId 角色主键Id。 + * @param sysUserDtoFilter 用户过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysRole.update") + @PostMapping("/listNotInUserRole") + public ResponseResult> listNotInUserRole( + @MyRequestBody Long roleId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doRoleUserVerify(roleId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = sysUserService.getNotInSysUserListByRoleId(roleId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 为指定角色添加用户列表。该操作可同时给一批用户赋值角色,并在同一事务内完成。 + * + * @param roleId 角色主键Id。 + * @param userIdListString 逗号分隔的用户Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysRole.update") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addUserRole") + public ResponseResult addUserRole(@MyRequestBody Long roleId, @MyRequestBody String userIdListString) { + if (MyCommonUtil.existBlankArgument(roleId, userIdListString)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + Set userIdSet = Arrays.stream( + userIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysRoleService.existId(roleId) + || !sysUserService.existUniqueKeyList("userId", userIdSet)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + List userRoleList = new LinkedList<>(); + for (Long userId : userIdSet) { + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(roleId); + userRole.setUserId(userId); + userRoleList.add(userRole); + } + sysRoleService.addUserRoleList(userRoleList); + return ResponseResult.success(); + } + + /** + * 为指定用户移除指定角色。 + * + * @param roleId 指定角色主键Id。 + * @param userId 指定用户主键Id。 + * @return 应答数据结果。 + */ + @SaCheckPermission("sysRole.update") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteUserRole") + public ResponseResult deleteUserRole(@MyRequestBody Long roleId, @MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(roleId, userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysRoleService.removeUserRole(roleId, userId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 以字典形式返回全部角色管理数据集合。字典的键值为[roleId, roleName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysRoleDto filter) { + List resultList = sysRoleService.getListByFilter(MyModelUtil.copyTo(filter, SysRole.class)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysRole::getRoleId, SysRole::getRoleName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = sysRoleService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysRole::getRoleId, SysRole::getRoleName)); + } + + private ResponseResult doRoleUserVerify(Long roleId) { + if (MyCommonUtil.existBlankArgument(roleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysRoleService.existId(roleId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java new file mode 100644 index 00000000..406898d2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java @@ -0,0 +1,378 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.alibaba.fastjson.TypeReference; +import cn.hutool.core.util.ReflectUtil; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreInfo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.vo.*; +import com.orangeforms.webadmin.upms.dto.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.webadmin.config.ApplicationConfig; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * 用户管理操作控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "用户管理管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysUser") +public class SysUserController { + + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private ApplicationConfig appConfig; + @Autowired + private SessionCacheHelper cacheHelper; + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private SysUserService sysUserService; + + /** + * 新增用户操作。 + * + * @param sysUserDto 新增用户对象。 + * @param deptPostIdListString 逗号分隔的部门岗位Id列表。 + * @param dataPermIdListString 逗号分隔的数据权限Id列表。 + * @param roleIdListString 逗号分隔的角色Id列表。 + * @return 应答结果对象,包含新增用户的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysUserDto.userId", + "sysUserDto.createTimeStart", + "sysUserDto.createTimeEnd"}) + @SaCheckPermission("sysUser.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody SysUserDto sysUserDto, + @MyRequestBody String deptPostIdListString, + @MyRequestBody String dataPermIdListString, + @MyRequestBody String roleIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysUserDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysUser sysUser = MyModelUtil.copyTo(sysUserDto, SysUser.class); + CallResult result = sysUserService.verifyRelatedData( + sysUser, null, roleIdListString, deptPostIdListString, dataPermIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set deptPostIdSet = result.getData().getObject("deptPostIdSet", new TypeReference>() {}); + Set roleIdSet = result.getData().getObject("roleIdSet", new TypeReference>() {}); + Set dataPermIdSet = result.getData().getObject("dataPermIdSet", new TypeReference>() {}); + sysUserService.saveNew(sysUser, roleIdSet, deptPostIdSet, dataPermIdSet); + return ResponseResult.success(sysUser.getUserId()); + } + + /** + * 更新用户操作。 + * + * @param sysUserDto 更新用户对象。 + * @param deptPostIdListString 逗号分隔的部门岗位Id列表。 + * @param dataPermIdListString 逗号分隔的数据权限Id列表。 + * @param roleIdListString 逗号分隔的角色Id列表。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysUserDto.createTimeStart", + "sysUserDto.createTimeEnd"}) + @SaCheckPermission("sysUser.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update( + @MyRequestBody SysUserDto sysUserDto, + @MyRequestBody String deptPostIdListString, + @MyRequestBody String dataPermIdListString, + @MyRequestBody String roleIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysUserDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysUser originalUser = sysUserService.getById(sysUserDto.getUserId()); + if (originalUser == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysUser sysUser = MyModelUtil.copyTo(sysUserDto, SysUser.class); + CallResult result = sysUserService.verifyRelatedData( + sysUser, originalUser, roleIdListString, deptPostIdListString, dataPermIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set roleIdSet = result.getData().getObject("roleIdSet", new TypeReference>() {}); + Set deptPostIdSet = result.getData().getObject("deptPostIdSet", new TypeReference>() {}); + Set dataPermIdSet = result.getData().getObject("dataPermIdSet", new TypeReference>() {}); + if (!sysUserService.update(sysUser, originalUser, roleIdSet, deptPostIdSet, dataPermIdSet)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 重置密码操作。 + * + * @param userId 指定用户主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysUser.resetPassword") + @PostMapping("/resetPassword") + public ResponseResult resetPassword(@MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysUserService.changePassword(userId, appConfig.getDefaultUserPassword())) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除用户管理数据。 + * + * @param userId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysUser.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + return this.doDelete(userId); + } + + /** + * 批量删除用户管理数据。 + * + * @param userIdList 待删除对象的主键Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysUser.delete") + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatch") + public ResponseResult deleteBatch(@MyRequestBody List userIdList) { + if (MyCommonUtil.existBlankArgument(userIdList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + for (Long userId : userIdList) { + ResponseResult responseResult = this.doDelete(userId); + if (!responseResult.isSuccess()) { + return responseResult; + } + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的用户管理列表。 + * + * @param sysUserDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("sysUser.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + SysUser sysUserFilter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List sysUserList = sysUserService.getSysUserListWithRelation(sysUserFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysUserList, SysUserVo.class)); + } + + /** + * 查看指定用户管理对象详情。 + * + * @param userId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("sysUser.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long userId) { + // 这里查看用户数据时候,需要把用户多对多关联的角色和数据权限Id一并查出。 + SysUser sysUser = sysUserService.getByIdWithRelation(userId, MyRelationParam.full()); + if (sysUser == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysUserVo sysUserVo = MyModelUtil.copyTo(sysUser, SysUserVo.class); + return ResponseResult.success(sysUserVo); + } + + /** + * 附件文件下载。 + * 这里将图片和其他类型的附件文件放到不同的父目录下,主要为了便于今后图片文件的迁移。 + * + * @param userId 附件所在记录的主键Id。 + * @param fieldName 附件所属的字段名。 + * @param filename 文件名。如果没有提供该参数,就从当前记录的指定字段中读取。 + * @param asImage 下载文件是否为图片。 + * @param response Http 应答对象。 + */ + @SaCheckPermission("sysUser.view") + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/download") + public void download( + @RequestParam(required = false) Long userId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) { + if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。 + // 否则有可能给前端返回的是200的错误码。 + try { + // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。 + if (userId == null) { + if (!cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } else { + SysUser sysUser = sysUserService.getById(userId); + if (sysUser == null) { + ResponseResult.output(HttpServletResponse.SC_NOT_FOUND); + return; + } + String fieldJsonData = (String) ReflectUtil.getFieldValue(sysUser, fieldName); + if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST); + return; + } + if (!BaseUpDownloader.containFile(fieldJsonData, filename) + && !cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, fieldName); + if (!storeInfo.isSupportUpload()) { + ResponseResult.output(HttpServletResponse.SC_NOT_IMPLEMENTED, + ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD)); + return; + } + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + upDownloader.doDownload(appConfig.getUploadFileBaseDir(), + SysUser.class.getSimpleName(), fieldName, filename, asImage, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + /** + * 文件上传操作。 + * + * @param fieldName 上传文件名。 + * @param asImage 是否作为图片上传。如果是图片,今后下载的时候无需权限验证。否则就是附件上传,下载时需要权限验证。 + * @param uploadFile 上传文件对象。 + */ + @SaCheckPermission("sysUser.view") + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/upload") + public void upload( + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, fieldName); + // 这里就会判断参数中指定的字段,是否支持上传操作。 + if (!storeInfo.isSupportUpload()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD)); + return; + } + // 根据字段注解中的存储类型,通过工厂方法获取匹配的上传下载实现类,从而解耦。 + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), fieldName, asImage, uploadFile); + if (Boolean.TRUE.equals(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + cacheHelper.putSessionUploadFile(responseInfo.getFilename()); + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + /** + * 以字典形式返回全部用户管理数据集合。字典的键值为[userId, showName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysUserDto filter) { + List resultList = + sysUserService.getListByFilter(MyModelUtil.copyTo(filter, SysUser.class)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysUser::getUserId, SysUser::getShowName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = sysUserService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysUser::getUserId, SysUser::getShowName)); + } + + private ResponseResult doDelete(Long userId) { + String errorMessage; + // 验证关联Id的数据合法性 + SysUser originalSysUser = sysUserService.getById(userId); + if (originalSysUser == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!sysUserService.remove(userId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java new file mode 100644 index 00000000..db58a68f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPermDept; + +/** + * 数据权限与部门关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermDeptMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java new file mode 100644 index 00000000..9483f952 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java @@ -0,0 +1,43 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPerm; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据权限数据访问操作接口。 + * NOTE: 该对象一定不能被 @EnableDataPerm 注解标注,否则会导致无限递归。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermMapper extends BaseDaoMapper { + + /** + * 获取数据权限列表。 + * + * @param sysDataPermFilter 过滤对象。 + * @param orderBy 排序字符串。 + * @return 过滤后的数据权限列表。 + */ + List getSysDataPermList( + @Param("sysDataPermFilter") SysDataPerm sysDataPermFilter, @Param("orderBy") String orderBy); + + /** + * 获取指定用户的数据权限列表。 + * + * @param userId 用户Id。 + * @return 数据权限列表。 + */ + List getSysDataPermListByUserId(@Param("userId") Long userId); + + /** + * 查询与指定菜单关联的数据权限列表。 + * + * @param menuId 菜单Id。 + * @return 与菜单Id关联的数据权限列表。 + */ + List getSysDataPermListByMenuId(@Param("menuId") Long menuId); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java new file mode 100644 index 00000000..37fa8274 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPermMenu; + +/** + * 数据权限与菜单关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermMenuMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java new file mode 100644 index 00000000..1ca7d6d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPermUser; + +/** + * 数据权限与用户关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermUserMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java new file mode 100644 index 00000000..9f0dc2c2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDept; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 部门管理数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptMapper extends BaseDaoMapper { + + /** + * 批量插入对象列表。 + * + * @param sysDeptList 新增对象列表。 + */ + void insertList(List sysDeptList); + + /** + * 获取过滤后的对象列表。 + * + * @param sysDeptFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysDeptList( + @Param("sysDeptFilter") SysDept sysDeptFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java new file mode 100644 index 00000000..93eb328a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDeptPost; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 部门岗位数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptPostMapper extends BaseDaoMapper { + + /** + * 获取指定部门Id的部门岗位多对多关联数据列表,以及关联的部门和岗位数据。 + * + * @param deptId 部门Id。如果参数为空则返回全部数据。 + * @return 部门岗位多对多数据列表。 + */ + List> getSysDeptPostListWithRelationByDeptId(@Param("deptId") Long deptId); + + /** + * 获取指定部门Id的领导部门岗位列表。 + * + * @param deptId 部门Id。 + * @return 指定部门Id的领导部门岗位列表 + */ + List getLeaderDeptPostList(@Param("deptId") Long deptId); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java new file mode 100644 index 00000000..a0f66281 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java @@ -0,0 +1,42 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDeptRelation; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 部门关系树关联关系表访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptRelationMapper extends BaseDaoMapper { + + /** + * 将myDeptId的所有子部门,与其父部门parentDeptId解除关联关系。 + * + * @param parentDeptIds myDeptId的父部门Id列表。 + * @param myDeptId 当前部门。 + */ + void removeBetweenChildrenAndParents( + @Param("parentDeptIds") List parentDeptIds, @Param("myDeptId") Long myDeptId); + + /** + * 批量插入部门关联数据。 + * 由于目前版本(3.4.1)的Mybatis Plus没有提供真正的批量插入,为了保证效率需要自己实现。 + * 目前我们仅仅给出MySQL和PostgresSQL的insert list实现作为参考,其他数据库需要自行修改。 + * + * @param deptRelationList 部门关联关系数据列表。 + */ + void insertList(List deptRelationList); + + /** + * 批量插入当前部门的所有父部门列表,包括自己和自己的关系。 + * + * @param parentDeptId myDeptId的父部门Id。 + * @param myDeptId 当前部门。 + */ + void insertParentList(@Param("parentDeptId") Long parentDeptId, @Param("myDeptId") Long myDeptId); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java new file mode 100644 index 00000000..da04a33c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java @@ -0,0 +1,40 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysMenu; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 菜单数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysMenuMapper extends BaseDaoMapper { + + /** + * 获取登录用户的菜单列表。 + * + * @param userId 登录用户。 + * @return 菜单列表。 + */ + List getMenuListByUserId(@Param("userId") Long userId); + + /** + * 获取指定角色Id集合的菜单列表。 + * + * @param roleIds 角色Id集合。 + * @return 菜单列表。 + */ + List getMenuListByRoleIds(@Param("roleIds") Set roleIds); + + /** + * 查询包含指定菜单编码的菜单数量,目前仅用于satoken的权限框架。 + * + * @param menuCode 菜单编码。 + * @return 查询数量 + */ + int countMenuCode(@Param("menuCode") String menuCode); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java new file mode 100644 index 00000000..52a78fbf --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysPermWhitelist; + +/** + * 权限资源白名单数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPermWhitelistMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java new file mode 100644 index 00000000..4d17cc24 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java @@ -0,0 +1,52 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysPost; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 岗位管理数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPostMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param sysPostFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysPostList( + @Param("sysPostFilter") SysPost sysPostFilter, @Param("orderBy") String orderBy); + + /** + * 获取指定部门的岗位列表。 + * + * @param deptId 部门Id。 + * @param sysPostFilter 从表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 岗位数据列表。 + */ + List getSysPostListByDeptId( + @Param("deptId") Long deptId, + @Param("sysPostFilter") SysPost sysPostFilter, + @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表中没有和主表建立关联关系的数据列表。 + * + * @param deptId 关联主表Id。 + * @param sysPostFilter 过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 与主表没有建立关联的从表数据列表。 + */ + List getNotInSysPostListByDeptId( + @Param("deptId") Long deptId, + @Param("sysPostFilter") SysPost sysPostFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java new file mode 100644 index 00000000..9187244e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java @@ -0,0 +1,25 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysRole; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 角色数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysRoleMapper extends BaseDaoMapper { + + /** + * 获取对象列表,过滤条件中包含like和between条件。 + * + * @param sysRoleFilter 过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysRoleList(@Param("sysRoleFilter") SysRole sysRoleFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java new file mode 100644 index 00000000..38e63912 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysRoleMenu; + +/** + * 角色与菜单操作关联关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysRoleMenuMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java new file mode 100644 index 00000000..055985d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java @@ -0,0 +1,188 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysUser; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 用户管理数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserMapper extends BaseDaoMapper { + + /** + * 批量插入对象列表。 + * + * @param sysUserList 新增对象列表。 + */ + void insertList(List sysUserList); + + /** + * 获取过滤后的对象列表。 + * + * @param sysUserFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysUserList( + @Param("sysUserFilter") SysUser sysUserFilter, @Param("orderBy") String orderBy); + + /** + * 根据部门Id集合,获取关联的用户列表。 + * + * @param deptIds 关联的部门Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和部门Id集合关联的用户列表。 + */ + List getSysUserListByDeptIds( + @Param("deptIds") Set deptIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据登录名集合,获取关联的用户列表。 + * @param loginNames 登录名集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和登录名集合关联的用户列表。 + */ + List getSysUserListByLoginNames( + @Param("loginNames") List loginNames, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据角色Id,获取关联的用户列表。 + * + * @param roleId 关联的角色Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和角色Id关联的用户列表。 + */ + List getSysUserListByRoleId( + @Param("roleId") Long roleId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据角色Id集合,获取去重后的用户Id列表。 + * + * @param roleIds 关联的角色Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和角色Id集合关联的去重后的用户Id列表。 + */ + List getUserIdListByRoleIds( + @Param("roleIds") Set roleIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据角色Id,获取和当前角色Id没有建立多对多关联关系的用户列表。 + * + * @param roleId 关联的角色Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和RoleId没有建立关联关系的用户列表。 + */ + List getNotInSysUserListByRoleId( + @Param("roleId") Long roleId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据数据权限Id,获取关联的用户列表。 + * + * @param dataPermId 关联的数据权限Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和DataPermId关联的用户列表。 + */ + List getSysUserListByDataPermId( + @Param("dataPermId") Long dataPermId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据数据权限Id,获取和当前数据权限Id没有建立多对多关联关系的用户列表。 + * + * @param dataPermId 关联的数据权限Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和DataPermId没有建立关联关系的用户列表。 + */ + List getNotInSysUserListByDataPermId( + @Param("dataPermId") Long dataPermId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据部门岗位Id集合,获取关联的去重后的用户Id列表。 + * + * @param deptPostIds 关联的部门岗位Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和部门岗位Id集合关联的去重后的用户Id列表。 + */ + List getUserIdListByDeptPostIds( + @Param("deptPostIds") Set deptPostIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据部门岗位Id,获取关联的用户列表。 + * + * @param deptPostId 关联的部门岗位Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和部门岗位Id关联的用户列表。 + */ + List getSysUserListByDeptPostId( + @Param("deptPostId") Long deptPostId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据部门岗位Id,获取和当前部门岗位Id没有建立多对多关联关系的用户列表。 + * + * @param deptPostId 关联的部门岗位Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和deptPostId没有建立关联关系的用户列表。 + */ + List getNotInSysUserListByDeptPostId( + @Param("deptPostId") Long deptPostId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据岗位Id集合,获取关联的去重后的用户Id列表。 + * + * @param postIds 关联的岗位Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和岗位Id集合关联的去重后的用户Id列表。 + */ + List getUserIdListByPostIds( + @Param("postIds") Set postIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据岗位Id,获取关联的用户列表。 + * + * @param postId 关联的岗位Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和岗位Id关联的用户列表。 + */ + List getSysUserListByPostId( + @Param("postId") Long postId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java new file mode 100644 index 00000000..6da64992 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysUserPost; + +/** + * 用户岗位数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserPostMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java new file mode 100644 index 00000000..bf6dcfb8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysUserRole; + +/** + * 用户与角色关联关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserRoleMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml new file mode 100644 index 00000000..d3b228e6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml new file mode 100644 index 00000000..02c2e688 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + AND zz_sys_data_perm.rule_type = #{sysDataPermFilter.ruleType} + + + + AND IFNULL(zz_sys_data_perm.data_perm_name, '') LIKE #{safeSearchString} + + + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml new file mode 100644 index 00000000..c668302f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml new file mode 100644 index 00000000..2530c39f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml new file mode 100644 index 00000000..ef63bdc9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + INSERT INTO zz_sys_dept + (dept_id, + dept_name, + show_order, + parent_id, + deleted_flag, + create_user_id, + update_user_id, + create_time, + update_time) + VALUES + + (#{item.deptId}, + #{item.deptName}, + #{item.showOrder}, + #{item.parentId}, + #{item.deletedFlag}, + #{item.createUserId}, + #{item.updateUserId}, + #{item.createTime}, + #{item.updateTime}) + + + + + + + + AND zz_sys_dept.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + + AND zz_sys_dept.dept_name LIKE #{safeSysDeptDeptName} + + + AND zz_sys_dept.parent_id = #{sysDeptFilter.parentId} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml new file mode 100644 index 00000000..5d03d88b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml new file mode 100644 index 00000000..37ebd397 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + DELETE a FROM zz_sys_dept_relation a + INNER JOIN zz_sys_dept_relation b ON a.dept_id = b.dept_id + WHERE b.parent_dept_id = #{myDeptId} AND a.parent_dept_id IN + + #{item} + + + + + INSERT INTO zz_sys_dept_relation(parent_dept_id, dept_id) VALUES + + (#{item.parentDeptId}, #{item.deptId}) + + + + + INSERT INTO zz_sys_dept_relation(parent_dept_id, dept_id) + SELECT t.parent_dept_id, #{myDeptId} FROM zz_sys_dept_relation t + WHERE t.dept_id = #{parentDeptId} + UNION ALL + SELECT #{myDeptId}, #{myDeptId} + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml new file mode 100644 index 00000000..d9ba9e7b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml new file mode 100644 index 00000000..00d0c6d4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml new file mode 100644 index 00000000..50765655 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_sys_post.post_name LIKE #{safeSysPostPostName} + + + AND zz_sys_post.leader_post = #{sysPostFilter.leaderPost} + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml new file mode 100644 index 00000000..26b8e587 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + AND role_name LIKE #{safeRoleName} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml new file mode 100644 index 00000000..6bf30195 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml new file mode 100644 index 00000000..162d6b2d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO zz_sys_user + (user_id, + login_name, + password, + dept_id, + show_name, + user_type, + head_image_url, + user_status, + email, + mobile, + create_user_id, + update_user_id, + create_time, + update_time, + deleted_flag) + VALUES + + (#{item.userId}, + #{item.loginName}, + #{item.password}, + #{item.deptId}, + #{item.showName}, + #{item.userType}, + #{item.headImageUrl}, + #{item.userStatus}, + #{item.email}, + #{item.mobile}, + #{item.createUserId}, + #{item.updateUserId}, + #{item.createTime}, + #{item.updateTime}, + #{item.deletedFlag}) + + + + + + + + AND zz_sys_user.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + + AND zz_sys_user.login_name LIKE #{safeSysUserLoginName} + + + AND (EXISTS (SELECT 1 FROM zz_sys_dept_relation WHERE + zz_sys_dept_relation.parent_dept_id = #{sysUserFilter.deptId} + AND zz_sys_user.dept_id = zz_sys_dept_relation.dept_id)) + + + + AND zz_sys_user.show_name LIKE #{safeSysUserShowName} + + + AND zz_sys_user.user_status = #{sysUserFilter.userStatus} + + + AND zz_sys_user.create_time >= #{sysUserFilter.createTimeStart} + + + AND zz_sys_user.create_time <= #{sysUserFilter.createTimeEnd} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml new file mode 100644 index 00000000..b846ba04 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml new file mode 100644 index 00000000..c4993db0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java new file mode 100644 index 00000000..69aa2867 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与部门关联Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与部门关联Dto") +@Data +public class SysDataPermDeptDto { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long dataPermId; + + /** + * 关联部门Id。 + */ + @Schema(description = "关联部门Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long deptId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java new file mode 100644 index 00000000..725c8068 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java @@ -0,0 +1,55 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.constant.DataPermRuleType; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 数据权限Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限Dto") +@Data +public class SysDataPermDto { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据权限Id不能为空!", groups = {UpdateGroup.class}) + private Long dataPermId; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据权限名称不能为空!") + private String dataPermName; + + /** + * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。 + */ + @Schema(description = "数据权限规则类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据权限规则类型不能为空!") + @ConstDictRef(constDictClass = DataPermRuleType.class) + private Integer ruleType; + + /** + * 部门Id列表(逗号分隔)。 + */ + @Schema(hidden = true) + private String deptIdListString; + + /** + * 搜索字符串。 + */ + @Schema(description = "LIKE 模糊搜索字符串") + private String searchString; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java new file mode 100644 index 00000000..763e9ddc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与菜单关联Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与菜单关联Dto") +@Data +public class SysDataPermMenuDto { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long dataPermId; + + /** + * 关联菜单Id。 + */ + @Schema(description = "关联菜单Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long menuId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java new file mode 100644 index 00000000..335f1607 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java @@ -0,0 +1,48 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 部门管理Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysDeptDto对象") +@Data +public class SysDeptDto { + + /** + * 部门Id。 + */ + @Schema(description = "部门Id。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,部门Id不能为空!", groups = {UpdateGroup.class}) + private Long deptId; + + /** + * 部门名称。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "部门名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,部门名称不能为空!") + private String deptName; + + /** + * 显示顺序。 + */ + @Schema(description = "显示顺序。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,显示顺序不能为空!") + private Integer showOrder; + + /** + * 父部门Id。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "父部门Id。可支持等于操作符的列表数据过滤。") + private Long parentId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java new file mode 100644 index 00000000..6362ebe8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 部门岗位Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "部门岗位Dto") +@Data +public class SysDeptPostDto { + + /** + * 部门岗位Id。 + */ + @Schema(description = "部门岗位Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,部门岗位Id不能为空!", groups = {UpdateGroup.class}) + private Long deptPostId; + + /** + * 部门Id。 + */ + @Schema(description = "部门Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,部门Id不能为空!", groups = {UpdateGroup.class}) + private Long deptId; + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,岗位Id不能为空!", groups = {UpdateGroup.class}) + private Long postId; + + /** + * 部门岗位显示名称。 + */ + @Schema(description = "部门岗位显示名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,部门岗位显示名称不能为空!") + private String postShowName; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java new file mode 100644 index 00000000..986f8dae --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java @@ -0,0 +1,92 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 菜单Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "菜单Dto") +@Data +public class SysMenuDto { + + /** + * 菜单Id。 + */ + @Schema(description = "菜单Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "菜单Id不能为空!", groups = {UpdateGroup.class}) + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null + */ + @Schema(description = "父菜单Id") + private Long parentId; + + /** + * 菜单显示名称。 + */ + @Schema(description = "菜单显示名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "菜单显示名称不能为空!") + private String menuName; + + /** + * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + @Schema(description = "菜单类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "菜单类型不能为空!") + @ConstDictRef(constDictClass = SysMenuType.class, message = "数据验证失败,菜单类型为无效值!") + private Integer menuType; + + /** + * 前端表单路由名称,仅用于menu_type为1的菜单类型。 + */ + @Schema(description = "前端表单路由名称") + private String formRouterName; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + @Schema(description = "在线表单主键Id") + private Long onlineFormId; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + @Schema(description = "统计页面主键Id") + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + @Schema(description = "仅用于在线表单的流程Id") + private Long onlineFlowEntryId; + + /** + * 菜单显示顺序 (值越小,排序越靠前)。 + */ + @Schema(description = "菜单显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "菜单显示顺序不能为空!") + private Integer showOrder; + + /** + * 菜单图标。 + */ + @Schema(description = "菜单显示图标") + private String icon; + + /** + * 附加信息。 + */ + @Schema(description = "附加信息") + private String extraData; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java new file mode 100644 index 00000000..c9bef765 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 岗位Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "岗位Dto") +@Data +public class SysPostDto { + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,岗位Id不能为空!", groups = {UpdateGroup.class}) + private Long postId; + + /** + * 岗位名称。 + */ + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,岗位名称不能为空!") + private String postName; + + /** + * 岗位层级,数值越小级别越高。 + */ + @Schema(description = "岗位层级", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,岗位层级不能为空!") + private Integer postLevel; + + /** + * 是否领导岗位。 + */ + @Schema(description = "是否领导岗位", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,领导岗位不能为空!", groups = {UpdateGroup.class}) + private Boolean leaderPost; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java new file mode 100644 index 00000000..3a567acd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java @@ -0,0 +1,32 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 角色Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "角色Dto") +@Data +public class SysRoleDto { + + /** + * 角色Id。 + */ + @Schema(description = "角色Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "角色Id不能为空!", groups = {UpdateGroup.class}) + private Long roleId; + + /** + * 角色名称。 + */ + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "角色名称不能为空!") + private String roleName; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java new file mode 100644 index 00000000..4a993689 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java @@ -0,0 +1,110 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.webadmin.upms.model.constant.SysUserType; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 用户管理Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysUserDto对象") +@Data +public class SysUserDto { + + /** + * 用户Id。 + */ + @Schema(description = "用户Id。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户Id不能为空!", groups = {UpdateGroup.class}) + private Long userId; + + /** + * 登录用户名。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "登录用户名。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,登录用户名不能为空!") + private String loginName; + + /** + * 用户密码。 + */ + @Schema(description = "用户密码。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,用户密码不能为空!", groups = {AddGroup.class}) + private String password; + + /** + * 用户部门Id。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "用户部门Id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户部门Id不能为空!") + private Long deptId; + + /** + * 用户显示名称。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "用户显示名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,用户显示名称不能为空!") + private String showName; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)不能为空!") + @ConstDictRef(constDictClass = SysUserType.class, message = "数据验证失败,用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)为无效值!") + private Integer userType; + + /** + * 用户头像的Url。 + */ + @Schema(description = "用户头像的Url。") + private String headImageUrl; + + /** + * 用户状态(0: 正常 1: 锁定)。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "用户状态(0: 正常 1: 锁定)。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户状态(0: 正常 1: 锁定)不能为空!") + @ConstDictRef(constDictClass = SysUserStatus.class, message = "数据验证失败,用户状态(0: 正常 1: 锁定)为无效值!") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + @Schema(description = "用户邮箱。") + private String email; + + /** + * 用户手机。 + */ + @Schema(description = "用户手机。") + private String mobile; + + /** + * createTime 范围过滤起始值(>=)。 + * NOTE: 可支持范围操作符的列表数据过滤。 + */ + @Schema(description = "createTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。") + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + * NOTE: 可支持范围操作符的列表数据过滤。 + */ + @Schema(description = "createTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。") + private String createTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java new file mode 100644 index 00000000..8f71c696 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java @@ -0,0 +1,62 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.annotation.RelationManyToMany; +import com.orangeforms.common.core.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.*; + +/** + * 数据权限实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table(value = "zz_sys_data_perm") +public class SysDataPerm extends BaseModel { + + /** + * 主键Id。 + */ + @Id(value = "data_perm_id") + private Long dataPermId; + + /** + * 显示名称。 + */ + @Column(value = "data_perm_name") + private String dataPermName; + + /** + * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。 + */ + @Column(value = "rule_type") + private Integer ruleType; + + @Column(ignore = true) + private String deptIdListString; + + @RelationManyToMany( + relationMasterIdField = "dataPermId", + relationModelClass = SysDataPermDept.class) + @Column(ignore = true) + private List dataPermDeptList; + + @RelationManyToMany( + relationMasterIdField = "dataPermId", + relationModelClass = SysDataPermMenu.class) + @Column(ignore = true) + private List dataPermMenuList; + + @Column(ignore = true) + private String searchString; + + public void setSearchString(String searchString) { + this.searchString = MyCommonUtil.replaceSqlWildcard(searchString); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java new file mode 100644 index 00000000..43b91b2a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java @@ -0,0 +1,29 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; +import lombok.ToString; + +/** + * 数据权限与部门关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString(of = {"deptId"}) +@Table(value = "zz_sys_data_perm_dept") +public class SysDataPermDept { + + /** + * 数据权限Id。 + */ + @Column(value = "data_perm_id") + private Long dataPermId; + + /** + * 关联部门Id。 + */ + @Column(value = "dept_id") + private Long deptId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java new file mode 100644 index 00000000..4976f769 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java @@ -0,0 +1,29 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; +import lombok.ToString; + +/** + * 数据权限与菜单关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString(of = {"menuId"}) +@Table(value = "zz_sys_data_perm_menu") +public class SysDataPermMenu { + + /** + * 数据权限Id。 + */ + @Column(value = "data_perm_id") + private Long dataPermId; + + /** + * 关联菜单Id。 + */ + @Column(value = "menu_id") + private Long menuId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java new file mode 100644 index 00000000..1f1e9f58 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 数据权限与用户关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_data_perm_user") +public class SysDataPermUser { + + /** + * 数据权限Id。 + */ + @Column(value = "data_perm_id") + private Long dataPermId; + + /** + * 用户Id。 + */ + @Column(value = "user_id") + private Long userId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java new file mode 100644 index 00000000..c50e4438 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java @@ -0,0 +1,73 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import lombok.Data; + +import java.util.Date; + +/** + * 部门管理实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_dept") +public class SysDept { + + /** + * 部门Id。 + */ + @Id(value = "dept_id") + private Long deptId; + + /** + * 部门名称。 + */ + @Column(value = "dept_name") + private String deptName; + + /** + * 显示顺序。 + */ + @Column(value = "show_order") + private Integer showOrder; + + /** + * 父部门Id。 + */ + @Column(value = "parent_id") + private Long parentId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java new file mode 100644 index 00000000..615ec2b1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 部门岗位多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_dept_post") +public class SysDeptPost { + + /** + * 部门岗位Id。 + */ + @Id(value = "dept_post_id") + private Long deptPostId; + + /** + * 部门Id。 + */ + @Column(value = "dept_id") + private Long deptId; + + /** + * 岗位Id。 + */ + @Column(value = "post_id") + private Long postId; + + /** + * 部门岗位显示名称。 + */ + @Column(value = "post_show_name") + private String postShowName; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java new file mode 100644 index 00000000..038daba0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java @@ -0,0 +1,31 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 部门关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Table(value = "zz_sys_dept_relation") +public class SysDeptRelation { + + /** + * 上级部门Id。 + */ + @Column(value = "parent_dept_id") + private Long parentDeptId; + + /** + * 部门Id。 + */ + @Column(value = "dept_id") + private Long deptId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java new file mode 100644 index 00000000..1797fc20 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java @@ -0,0 +1,96 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.base.model.BaseModel; +import com.orangeforms.webadmin.upms.bo.SysMenuExtraData; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table(value = "zz_sys_menu") +public class SysMenu extends BaseModel { + + /** + * 菜单Id。 + */ + @Id(value = "menu_id") + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null。 + */ + @Column(value = "parent_id") + private Long parentId; + + /** + * 菜单显示名称。 + */ + @Column(value = "menu_name") + private String menuName; + + /** + * 菜单类型(0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + @Column(value = "menu_type") + private Integer menuType; + + /** + * 前端表单路由名称,仅用于menu_type为1的菜单类型。 + */ + @Column(value = "form_router_name") + private String formRouterName; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + @Column(value = "online_form_id") + private Long onlineFormId; + + /** + * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。 + */ + @Column(value = "online_menu_perm_type") + private Integer onlineMenuPermType; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + @Column(value = "report_page_id") + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + @Column(value = "online_flow_entry_id") + private Long onlineFlowEntryId; + + /** + * 菜单显示顺序 (值越小,排序越靠前)。 + */ + @Column(value = "show_order") + private Integer showOrder; + + /** + * 菜单图标。 + */ + private String icon; + + /** + * 附加信息。 + */ + @Column(value = "extra_data") + private String extraData; + + /** + * extraData字段解析后的对象数据。 + */ + @Column(ignore = true) + private SysMenuExtraData extraObject; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java new file mode 100644 index 00000000..05c4457b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 白名单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_perm_whitelist") +public class SysPermWhitelist { + + /** + * 权限资源的URL。 + */ + @Id(value = "perm_url") + private String permUrl; + + /** + * 权限资源所属模块名字(通常是Controller的名字)。 + */ + @Column(value = "module_name") + private String moduleName; + + /** + * 权限的名称。 + */ + @Column(value = "perm_name") + private String permName; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java new file mode 100644 index 00000000..57368f0e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java @@ -0,0 +1,48 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 岗位实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table(value = "zz_sys_post") +public class SysPost extends BaseModel { + + /** + * 岗位Id。 + */ + @Id(value = "post_id") + private Long postId; + + /** + * 岗位名称。 + */ + @Column(value = "post_name") + private String postName; + + /** + * 岗位层级,数值越小级别越高。 + */ + @Column(value = "post_level") + private Integer postLevel; + + /** + * 是否领导岗位。 + */ + @Column(value = "leader_post") + private Boolean leaderPost; + + /** + * postId 的多对多关联表数据对象。 + */ + @Column(ignore = true) + private SysDeptPost sysDeptPost; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java new file mode 100644 index 00000000..62d94183 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationManyToMany; +import com.orangeforms.common.core.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.*; + +/** + * 角色实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table(value = "zz_sys_role") +public class SysRole extends BaseModel { + + /** + * 角色Id。 + */ + @Id(value = "role_id") + private Long roleId; + + /** + * 角色名称。 + */ + @Column(value = "role_name") + private String roleName; + + @RelationManyToMany( + relationMasterIdField = "roleId", + relationModelClass = SysRoleMenu.class) + @Column(ignore = true) + private List sysRoleMenuList; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java new file mode 100644 index 00000000..5fcc9065 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 角色菜单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_role_menu") +public class SysRoleMenu { + + /** + * 角色Id。 + */ + @Column(value = "role_id") + private Long roleId; + + /** + * 菜单Id。 + */ + @Column(value = "menu_id") + private Long menuId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java new file mode 100644 index 00000000..fe3e132f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java @@ -0,0 +1,172 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import com.orangeforms.webadmin.upms.model.constant.SysUserType; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.core.annotation.*; +import lombok.Data; + +import java.util.Date; +import java.util.Map; +import java.util.List; + +/** + * 用户管理实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_user") +public class SysUser { + + /** + * 用户Id。 + */ + @Id(value = "user_id") + private Long userId; + + /** + * 登录用户名。 + */ + @Column(value = "login_name") + private String loginName; + + /** + * 用户密码。 + */ + private String password; + + /** + * 用户部门Id。 + */ + @Column(value = "dept_id") + private Long deptId; + + /** + * 用户显示名称。 + */ + @Column(value = "show_name") + private String showName; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Column(value = "user_type") + private Integer userType; + + /** + * 用户头像的Url。 + */ + @UploadFlagColumn(storeType = UploadStoreTypeEnum.LOCAL_SYSTEM) + @Column(value = "head_image_url") + private String headImageUrl; + + /** + * 用户状态(0: 正常 1: 锁定)。 + */ + @Column(value = "user_status") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + private String email; + + /** + * 用户手机。 + */ + private String mobile; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @Column(ignore = true) + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @Column(ignore = true) + private String createTimeEnd; + + /** + * 多对多用户部门岗位数据集合。 + */ + @RelationManyToMany( + relationMasterIdField = "userId", + relationModelClass = SysUserPost.class) + @Column(ignore = true) + private List sysUserPostList; + + /** + * 多对多用户角色数据集合。 + */ + @RelationManyToMany( + relationMasterIdField = "userId", + relationModelClass = SysUserRole.class) + @Column(ignore = true) + private List sysUserRoleList; + + /** + * 多对多用户数据权限数据集合。 + */ + @RelationManyToMany( + relationMasterIdField = "userId", + relationModelClass = SysDataPermUser.class) + @Column(ignore = true) + private List sysDataPermUserList; + + @RelationDict( + masterIdField = "deptId", + slaveModelClass = SysDept.class, + slaveIdField = "deptId", + slaveNameField = "deptName") + @Column(ignore = true) + private Map deptIdDictMap; + + @RelationConstDict( + masterIdField = "userType", + constantDictClass = SysUserType.class) + @Column(ignore = true) + private Map userTypeDictMap; + + @RelationConstDict( + masterIdField = "userStatus", + constantDictClass = SysUserStatus.class) + @Column(ignore = true) + private Map userStatusDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java new file mode 100644 index 00000000..449e156e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 用户岗位多对多关系实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_user_post") +public class SysUserPost { + + /** + * 用户Id。 + */ + @Column(value = "user_id") + private Long userId; + + /** + * 部门岗位Id。 + */ + @Column(value = "dept_post_id") + private Long deptPostId; + + /** + * 岗位Id。 + */ + @Column(value = "post_id") + private Long postId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java new file mode 100644 index 00000000..c0ec2622 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 用户角色实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_sys_user_role") +public class SysUserRole { + + /** + * 用户Id。 + */ + @Column(value = "user_id") + private Long userId; + + /** + * 角色Id。 + */ + @Column(value = "role_id") + private Long roleId; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java new file mode 100644 index 00000000..6108183d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java @@ -0,0 +1,54 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 菜单类型常量对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysMenuType { + + /** + * 目录菜单。 + */ + public static final int TYPE_DIRECTORY = 0; + /** + * 普通菜单。 + */ + public static final int TYPE_MENU = 1; + /** + * 表单片段类型。 + */ + public static final int TYPE_UI_FRAGMENT = 2; + /** + * 按钮类型。 + */ + public static final int TYPE_BUTTON = 3; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(TYPE_DIRECTORY, "目录菜单"); + DICT_MAP.put(TYPE_MENU, "普通菜单"); + DICT_MAP.put(TYPE_UI_FRAGMENT, "表单片段类型"); + DICT_MAP.put(TYPE_BUTTON, "按钮类型"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysMenuType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java new file mode 100644 index 00000000..752ce7dd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java @@ -0,0 +1,44 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 菜单关联在线表单的控制权限类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysOnlineMenuPermType { + + /** + * 查看。 + */ + public static final int TYPE_VIEW = 0; + /** + * 编辑。 + */ + public static final int TYPE_EDIT = 1; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(TYPE_VIEW, "查看"); + DICT_MAP.put(TYPE_EDIT, "编辑"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysOnlineMenuPermType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java new file mode 100644 index 00000000..b71dd0aa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java @@ -0,0 +1,44 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 用户状态常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysUserStatus { + + /** + * 正常状态。 + */ + public static final int STATUS_NORMAL = 0; + /** + * 锁定状态。 + */ + public static final int STATUS_LOCKED = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(STATUS_NORMAL, "正常状态"); + DICT_MAP.put(STATUS_LOCKED, "锁定状态"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysUserStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java new file mode 100644 index 00000000..ee6fa852 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java @@ -0,0 +1,49 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 用户类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysUserType { + + /** + * 管理员。 + */ + public static final int TYPE_ADMIN = 0; + /** + * 系统操作员。 + */ + public static final int TYPE_SYSTEM = 1; + /** + * 普通操作员。 + */ + public static final int TYPE_OPERATOR = 2; + + private static final Map DICT_MAP = new HashMap<>(3); + static { + DICT_MAP.put(TYPE_ADMIN, "管理员"); + DICT_MAP.put(TYPE_SYSTEM, "系统操作员"); + DICT_MAP.put(TYPE_OPERATOR, "普通操作员"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysUserType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java new file mode 100644 index 00000000..0dff4fa6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java @@ -0,0 +1,114 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.webadmin.upms.model.*; + +import java.util.*; + +/** + * 数据权限数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermService extends IBaseService { + + /** + * 保存新增的数据权限对象。 + * + * @param dataPerm 新增的数据权限对象。 + * @param deptIdSet 关联的部门Id列表。 + * @param menuIdSet 关联的菜单Id列表。 + * @return 新增后的数据权限对象。 + */ + SysDataPerm saveNew(SysDataPerm dataPerm, Set deptIdSet, Set menuIdSet); + + /** + * 更新数据权限对象。 + * + * @param dataPerm 更新的数据权限对象。 + * @param originalDataPerm 原有的数据权限对象。 + * @param deptIdSet 关联的部门Id列表。 + * @param menuIdSet 关联的菜单Id列表。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysDataPerm dataPerm, SysDataPerm originalDataPerm, Set deptIdSet, Set menuIdSet); + + /** + * 删除指定数据权限。 + * + * @param dataPermId 数据权限主键Id。 + * @return 删除成功返回true,否则false。 + */ + boolean remove(Long dataPermId); + + /** + * 获取数据权限列表及其关联数据。 + * + * @param filter 数据权限过滤对象。 + * @param orderBy 排序参数。 + * @return 数据权限查询列表。 + */ + List getSysDataPermListWithRelation(SysDataPerm filter, String orderBy); + + /** + * 将指定用户的指定会话的数据权限集合存入缓存。 + * + * @param sessionId 会话Id。 + * @param userId 用户主键Id。 + * @param deptId 用户所属部门主键Id。 + */ + void putDataPermCache(String sessionId, Long userId, Long deptId); + + /** + * 将指定会话的数据权限集合从缓存中移除。 + * + * @param sessionId 会话Id。 + */ + void removeDataPermCache(String sessionId); + + /** + * 获取指定用户Id的数据权限列表。并基于menuId和权限规则类型进行了一级分组。 + * + * @param userId 指定的用户Id。 + * @param deptId 用户所属部门主键Id。 + * @return 合并优化后的数据权限列表。返回格式为,Map>。 + */ + Map> getSysDataPermListByUserId(Long userId, Long deptId); + + /** + * 查询与指定菜单关联的数据权限列表。 + * + * @param menuId 菜单Id。 + * @return 与菜单Id关联的数据权限列表。 + */ + List getSysDataPermListByMenuId(Long menuId); + + /** + * 添加用户和数据权限之间的多对多关联关系。 + * + * @param dataPermId 数据权限Id。 + * @param userIdSet 关联的用户Id列表。 + */ + void addDataPermUserList(Long dataPermId, Set userIdSet); + + /** + * 移除用户和数据权限之间的多对多关联关系。 + * + * @param dataPermId 数据权限主键Id。 + * @param userId 用户主键Id。 + * @return true移除成功,否则false。 + */ + boolean removeDataPermUser(Long dataPermId, Long userId); + + /** + * 验证数据权限对象关联菜单数据是否都合法。 + * + * @param dataPerm 数据权限关对象。 + * @param deptIdListString 与数据权限关联的部门Id列表。 + * @param menuIdListString 与数据权限关联的菜单Id列表。 + * @return 验证结果。 + */ + CallResult verifyRelatedData(SysDataPerm dataPerm, String deptIdListString, String menuIdListString); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java new file mode 100644 index 00000000..2a485df5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java @@ -0,0 +1,170 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 部门管理数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptService extends IBaseService { + + /** + * 保存新增的部门对象。 + * + * @param sysDept 新增的部门对象。 + * @param parentSysDept 上级部门对象。 + * @return 新增后的部门对象。 + */ + SysDept saveNew(SysDept sysDept, SysDept parentSysDept); + + /** + * 更新部门对象。 + * + * @param sysDept 更新的部门对象。 + * @param originalSysDept 原有的部门对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysDept sysDept, SysDept originalSysDept); + + /** + * 删除指定数据。 + * + * @param deptId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long deptId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getSysDeptListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysDeptList(SysDept filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getSysDeptList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysDeptListWithRelation(SysDept filter, String orderBy); + + /** + * 判断指定对象是否包含下级对象。 + * + * @param deptId 主键Id。 + * @return 存在返回true,否则false。 + */ + boolean hasChildren(Long deptId); + + /** + * 判断指定部门Id是否包含用户对象。 + * + * @param deptId 部门主键Id。 + * @return 存在返回true,否则false。 + */ + boolean hasChildrenUser(Long deptId); + + /** + * 批量添加多对多关联关系。 + * + * @param sysDeptPostList 多对多关联表对象集合。 + * @param deptId 主表Id。 + */ + void addSysDeptPostList(List sysDeptPostList, Long deptId); + + /** + * 更新中间表数据。 + * + * @param sysDeptPost 中间表对象。 + * @return 更新成功与否。 + */ + boolean updateSysDeptPost(SysDeptPost sysDeptPost); + + /** + * 移除单条多对多关系。 + * + * @param deptId 主表Id。 + * @param postId 从表Id。 + * @return 成功返回true,否则false。 + */ + boolean removeSysDeptPost(Long deptId, Long postId); + + /** + * 获取中间表数据。 + * + * @param deptId 主表Id。 + * @param postId 从表Id。 + * @return 中间表对象。 + */ + SysDeptPost getSysDeptPost(Long deptId, Long postId); + + /** + * 根据部门岗位Id获取部门岗位关联对象。 + * + * @param deptPostId 部门岗位Id。 + * @return 部门岗位对象。 + */ + SysDeptPost getSysDeptPost(Long deptPostId); + + /** + * 获取指定部门Id的部门岗位多对多关联数据列表,以及关联的部门和岗位数据。 + * + * @param deptId 部门Id。如果参数为空则返回全部数据。 + * @return 部门岗位多对多数据列表。 + */ + List> getSysDeptPostListWithRelationByDeptId(Long deptId); + + /** + * 获取指定部门Id和岗位Id集合的部门岗位多对多关联数据列表。 + * + * @param deptId 部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 部门岗位多对多数据列表。 + */ + List getSysDeptPostList(Long deptId, Set postIdSet); + + /** + * 获取与指定部门Id同级部门和岗位Id集合的部门岗位多对多关联数据列表。 + * + * @param deptId 部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 部门岗位多对多数据列表。 + */ + List getSiblingSysDeptPostList(Long deptId, Set postIdSet); + + /** + * 根据部门Id获取该部门领导岗位的部门岗位Id集合。 + * + * @param deptId 部门Id。 + * @return 部门领导岗位的部门岗位Id集合。 + */ + List getLeaderDeptPostIdList(Long deptId); + + /** + * 根据部门Id获取上级部门领导岗位的部门岗位Id集合。 + * + * @param deptId 部门Id。 + * @return 上级部门领导岗位的部门岗位Id集合。 + */ + List getUpLeaderDeptPostIdList(Long deptId); + + /** + * 根据父主键Id列表,获取当前部门Id及其所有下级部门Id列表。 + * + * @param parentIds 父主键Id列表。 + * @return 获取当前部门Id及其所有下级部门Id列表。 + */ + List getAllChildDeptIdByParentIds(List parentIds); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java new file mode 100644 index 00000000..7c39d7e8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java @@ -0,0 +1,72 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.webadmin.upms.model.SysMenu; + +import java.util.*; + +/** + * 菜单数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysMenuService extends IBaseService { + + /** + * 保存新增的菜单对象。 + * + * @param sysMenu 新增的菜单对象。 + * @return 新增后的菜单对象。 + */ + SysMenu saveNew(SysMenu sysMenu); + + /** + * 更新菜单对象。 + * + * @param sysMenu 更新的菜单对象。 + * @param originalSysMenu 原有的菜单对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysMenu sysMenu, SysMenu originalSysMenu); + + /** + * 删除指定的菜单。 + * + * @param menu 菜单对象。 + * @return 删除成功返回true,否则false。 + */ + boolean remove(SysMenu menu); + + /** + * 获取指定用户Id的菜单列表,已去重。 + * + * @param userId 用户主键Id。 + * @return 用户关联的菜单列表。 + */ + Collection getMenuListByUserId(Long userId); + + /** + * 根据角色Id集合获取菜单对象列表。 + * + * @param roleIds 逗号分隔的角色Id集合。 + * @return 菜单对象列表。 + */ + Collection getMenuListByRoleIds(String roleIds); + + /** + * 判断当前菜单是否存在子菜单。 + * + * @param menuId 菜单主键Id。 + * @return 存在返回true,否则false。 + */ + boolean hasChildren(Long menuId); + + /** + * 获取指定类型的所有在线表单的菜单。 + * + * @param menuType 菜单类型,NULL则返回全部类型。 + * @return 在线表单关联的菜单列表。 + */ + List getAllOnlineMenuList(Integer menuType); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java new file mode 100644 index 00000000..84dab9fa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java @@ -0,0 +1,23 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.webadmin.upms.model.SysPermWhitelist; + +import java.util.List; + +/** + * 权限资源白名单数据服务接口。 + * 白名单中的权限资源,可以不受权限控制,任何用户皆可访问,一般用于常用的字典数据列表接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPermWhitelistService extends IBaseService { + + /** + * 获取白名单权限资源的列表。 + * + * @return 白名单权限资源地址列表。 + */ + List getWhitelistPermList(); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java new file mode 100644 index 00000000..71165759 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java @@ -0,0 +1,99 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.webadmin.upms.model.SysPost; +import com.orangeforms.webadmin.upms.model.SysUserPost; + +import java.util.*; + +/** + * 岗位管理数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPostService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param sysPost 新增对象。 + * @return 返回新增对象。 + */ + SysPost saveNew(SysPost sysPost); + + /** + * 更新数据对象。 + * + * @param sysPost 更新的对象。 + * @param originalSysPost 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(SysPost sysPost, SysPost originalSysPost); + + /** + * 删除指定数据。 + * + * @param postId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long postId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getSysPostListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysPostList(SysPost filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getSysPostList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysPostListWithRelation(SysPost filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。 + * + * @param deptId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getNotInSysPostListByDeptId(Long deptId, SysPost filter, String orderBy); + + /** + * 获取指定部门的岗位列表。 + * + * @param deptId 部门Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysPostListByDeptId(Long deptId, SysPost filter, String orderBy); + + /** + * 获取指定用户的用户岗位多对多关联数据列表。 + * + * @param userId 用户Id。 + * @return 用户岗位多对多关联数据列表。 + */ + List getSysUserPostListByUserId(Long userId); + + /** + * 判断指定的部门岗位Id集合是否都属于指定的部门Id。 + * + * @param deptPostIdSet 部门岗位Id集合。 + * @param deptId 部门Id。 + * @return 全部是返回true,否则false。 + */ + boolean existAllPrimaryKeys(Set deptPostIdSet, Long deptId); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java new file mode 100644 index 00000000..1f6762d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java @@ -0,0 +1,87 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.webadmin.upms.model.SysRole; +import com.orangeforms.webadmin.upms.model.SysUserRole; + +import java.util.*; + +/** + * 角色数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysRoleService extends IBaseService { + + /** + * 保存新增的角色对象。 + * + * @param role 新增的角色对象。 + * @param menuIdSet 菜单Id列表。 + * @return 新增后的角色对象。 + */ + SysRole saveNew(SysRole role, Set menuIdSet); + + /** + * 更新角色对象。 + * + * @param role 更新的角色对象。 + * @param originalRole 原有的角色对象。 + * @param menuIdSet 菜单Id列表。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysRole role, SysRole originalRole, Set menuIdSet); + + /** + * 删除指定角色。 + * + * @param roleId 角色主键Id。 + * @return 删除成功返回true,否则false。 + */ + boolean remove(Long roleId); + + /** + * 获取角色列表。 + * + * @param filter 角色过滤对象。 + * @param orderBy 排序参数。 + * @return 角色列表。 + */ + List getSysRoleList(SysRole filter, String orderBy); + + /** + * 获取用户的用户角色对象列表。 + * + * @param userId 用户Id。 + * @return 用户角色对象列表。 + */ + List getSysUserRoleListByUserId(Long userId); + + /** + * 批量新增用户角色关联。 + * + * @param userRoleList 用户角色关系数据列表。 + */ + void addUserRoleList(List userRoleList); + + /** + * 移除指定用户和指定角色的关联关系。 + * + * @param roleId 角色主键Id。 + * @param userId 用户主键Id。 + * @return 移除成功返回true,否则false。 + */ + boolean removeUserRole(Long roleId, Long userId); + + /** + * 验证角色对象关联的数据是否都合法。 + * + * @param sysRole 当前操作的对象。 + * @param originalSysRole 原有对象。 + * @param menuIdListString 逗号分隔的menuId列表。 + * @return 验证结果。 + */ + CallResult verifyRelatedData(SysRole sysRole, SysRole originalSysRole, String menuIdListString); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java new file mode 100644 index 00000000..15fa2ea2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java @@ -0,0 +1,176 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 用户管理数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserService extends IBaseService { + + /** + * 获取指定登录名的用户对象。 + * + * @param loginName 指定登录用户名。 + * @return 用户对象。 + */ + SysUser getSysUserByLoginName(String loginName); + + /** + * 保存新增的用户对象。 + * + * @param user 新增的用户对象。 + * @param roleIdSet 用户角色Id集合。 + * @param deptPostIdSet 部门岗位Id集合。 + * @param dataPermIdSet 数据权限Id集合。 + * @return 新增后的用户对象。 + */ + SysUser saveNew(SysUser user, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet); + + /** + * 更新用户对象。 + * + * @param user 更新的用户对象。 + * @param originalUser 原有的用户对象。 + * @param roleIdSet 用户角色Id列表。 + * @param deptPostIdSet 部门岗位Id集合。 + * @param dataPermIdSet 数据权限Id集合。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysUser user, SysUser originalUser, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet); + + /** + * 修改用户密码。 + * @param userId 用户主键Id。 + * @param newPass 新密码。 + * @return 成功返回true,否则false。 + */ + boolean changePassword(Long userId, String newPass); + + /** + * 修改用户头像。 + * + * @param userId 用户主键Id。 + * @param newHeadImage 新的头像信息。 + * @return 成功返回true,否则false。 + */ + boolean changeHeadImage(Long userId, String newHeadImage); + + /** + * 删除指定数据。 + * + * @param userId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long userId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getSysUserListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysUserList(SysUser filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getSysUserList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysUserListWithRelation(SysUser filter, String orderBy); + + /** + * 获取指定角色的用户列表。 + * + * @param roleId 角色主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByRoleId(Long roleId, SysUser filter, String orderBy); + + /** + * 获取不属于指定角色的用户列表。 + * + * @param roleId 角色主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getNotInSysUserListByRoleId(Long roleId, SysUser filter, String orderBy); + + /** + * 获取指定数据权限的用户列表。 + * + * @param dataPermId 数据权限主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy); + + /** + * 获取不属于指定数据权限的用户列表。 + * + * @param dataPermId 数据权限主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getNotInSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy); + + /** + * 获取指定部门岗位的用户列表。 + * + * @param deptPostId 部门岗位主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy); + + /** + * 获取不属于指定部门岗位的用户列表。 + * + * @param deptPostId 部门岗位主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getNotInSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy); + + /** + * 获取指定岗位的用户列表。 + * + * @param postId 岗位主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByPostId(Long postId, SysUser filter, String orderBy); + + /** + * 验证用户对象关联的数据是否都合法。 + * + * @param sysUser 当前操作的对象。 + * @param originalSysUser 原有对象。 + * @param roleIds 逗号分隔的角色Id列表字符串。 + * @param deptPostIds 逗号分隔的部门岗位Id列表字符串。 + * @param dataPermIds 逗号分隔的数据权限Id列表字符串。 + * @return 验证结果。 + */ + CallResult verifyRelatedData( + SysUser sysUser, SysUser originalSysUser, String roleIds, String deptPostIds, String dataPermIds); +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java new file mode 100644 index 00000000..425a5606 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java @@ -0,0 +1,335 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.core.constant.DataPermRuleType; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.webadmin.config.ApplicationConfig; +import com.orangeforms.webadmin.upms.dao.SysDataPermDeptMapper; +import com.orangeforms.webadmin.upms.dao.SysDataPermMapper; +import com.orangeforms.webadmin.upms.dao.SysDataPermUserMapper; +import com.orangeforms.webadmin.upms.dao.SysDataPermMenuMapper; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.service.SysDataPermService; +import com.orangeforms.webadmin.upms.service.SysDeptService; +import com.orangeforms.webadmin.upms.service.SysMenuService; +import com.orangeforms.webadmin.upms.service.SysUserService; +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.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 数据权限数据服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysDataPermService") +public class SysDataPermServiceImpl extends BaseService implements SysDataPermService { + + @Autowired + private SysDataPermMapper sysDataPermMapper; + @Autowired + private SysDataPermDeptMapper sysDataPermDeptMapper; + @Autowired + private SysDataPermUserMapper sysDataPermUserMapper; + @Autowired + private SysDataPermMenuMapper sysDataPermMenuMapper; + @Autowired + private SysUserService sysUserService; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysMenuService sysMenuService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private ApplicationConfig applicationConfig; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysDataPermMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysDataPerm saveNew(SysDataPerm dataPerm, Set deptIdSet, Set menuIdSet) { + dataPerm.setDataPermId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(dataPerm); + sysDataPermMapper.insert(dataPerm); + this.insertRelationData(dataPerm, deptIdSet, menuIdSet); + return dataPerm; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update( + SysDataPerm dataPerm, SysDataPerm originalDataPerm, Set deptIdSet, Set menuIdSet) { + MyModelUtil.fillCommonsForUpdate(dataPerm, originalDataPerm); + if (sysDataPermMapper.update(dataPerm, false) != 1) { + return false; + } + sysDataPermDeptMapper.deleteByQuery( + new QueryWrapper().eq(SysDataPermDept::getDataPermId, dataPerm.getDataPermId())); + sysDataPermMenuMapper.deleteByQuery( + new QueryWrapper().eq(SysDataPermMenu::getDataPermId, dataPerm.getDataPermId())); + this.insertRelationData(dataPerm, deptIdSet, menuIdSet); + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dataPermId) { + if (sysDataPermMapper.deleteById(dataPermId) != 1) { + return false; + } + sysDataPermDeptMapper.deleteByQuery(new QueryWrapper().eq(SysDataPermDept::getDataPermId, dataPermId)); + sysDataPermUserMapper.deleteByQuery(new QueryWrapper().eq(SysDataPermUser::getDataPermId, dataPermId)); + sysDataPermMenuMapper.deleteByQuery(new QueryWrapper().eq(SysDataPermMenu::getDataPermId, dataPermId)); + return true; + } + + @Override + public List getSysDataPermListWithRelation(SysDataPerm filter, String orderBy) { + List resultList = sysDataPermMapper.getSysDataPermList(filter, orderBy); + buildRelationForDataList(resultList, MyRelationParam.full(), CollUtil.newHashSet("dataPermDeptList")); + return resultList; + } + + @Override + public void putDataPermCache(String sessionId, Long userId, Long deptId) { + Map> menuDataPermMap = getSysDataPermListByUserId(userId, deptId); + if (menuDataPermMap.size() > 0) { + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(sessionId); + RBucket bucket = redissonClient.getBucket(dataPermSessionKey); + bucket.set(JSON.toJSONString(menuDataPermMap), + applicationConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + } + } + + @Override + public void removeDataPermCache(String sessionId) { + String sessionPermKey = RedisKeyUtil.makeSessionDataPermIdKey(sessionId); + redissonClient.getBucket(sessionPermKey).deleteAsync(); + } + + @Override + public Map> getSysDataPermListByUserId(Long userId, Long deptId) { + List dataPermList = sysDataPermMapper.getSysDataPermListByUserId(userId); + dataPermList.forEach(dataPerm -> { + if (CollUtil.isNotEmpty(dataPerm.getDataPermDeptList())) { + Set deptIdSet = dataPerm.getDataPermDeptList().stream() + .map(SysDataPermDept::getDeptId).collect(Collectors.toSet()); + dataPerm.setDeptIdListString(StrUtil.join(",", deptIdSet)); + } + }); + Map> menuIdMap = new HashMap<>(4); + for (SysDataPerm dataPerm : dataPermList) { + if (CollUtil.isNotEmpty(dataPerm.getDataPermMenuList())) { + for (SysDataPermMenu dataPermMenu : dataPerm.getDataPermMenuList()) { + menuIdMap.computeIfAbsent( + dataPermMenu.getMenuId().toString(), k -> new LinkedList<>()).add(dataPerm); + } + } else { + menuIdMap.computeIfAbsent( + ApplicationConstant.DATA_PERM_ALL_MENU_ID, k -> new LinkedList<>()).add(dataPerm); + } + } + Map> menuResultMap = new HashMap<>(menuIdMap.size()); + for (Map.Entry> entry : menuIdMap.entrySet()) { + Map resultMap = this.mergeAndOptimizeDataPermRule(entry.getValue(), deptId); + menuResultMap.put(entry.getKey(), resultMap); + } + return menuResultMap; + } + + @Override + public List getSysDataPermListByMenuId(Long menuId) { + return sysDataPermMapper.getSysDataPermListByMenuId(menuId); + } + + private Map mergeAndOptimizeDataPermRule(List dataPermList, Long deptId) { + // 为了更方便进行后续的合并优化处理,这里再基于菜单Id和规则类型进行分组。ruleMap的key是规则类型。 + Map> ruleMap = + dataPermList.stream().collect(Collectors.groupingBy(SysDataPerm::getRuleType)); + Map resultMap = new HashMap<>(ruleMap.size()); + // 如有有ALL存在,就可以直接退出了,没有必要在处理后续的规则了。 + if (ruleMap.containsKey(DataPermRuleType.TYPE_ALL)) { + resultMap.put(DataPermRuleType.TYPE_ALL, "null"); + return resultMap; + } + // 这里优先合并最复杂的多部门及子部门场景。 + String deptIds = processMultiDeptAndChildren(ruleMap, deptId); + if (deptIds != null) { + resultMap.put(DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT, deptIds); + } + // 合并当前部门及子部门的优化 + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) != null) { + // 需要与仅仅当前部门规则进行合并。 + ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY); + resultMap.put(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT, "null"); + } + // 合并自定义部门了。 + deptIds = processMultiDept(ruleMap, deptId); + if (deptIds != null) { + resultMap.put(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, deptIds); + } + // 最后处理当前部门和当前用户。 + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_ONLY) != null) { + resultMap.put(DataPermRuleType.TYPE_DEPT_ONLY, "null"); + } + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS) != null) { + // 合并当前部门用户和当前用户 + ruleMap.remove(DataPermRuleType.TYPE_USER_ONLY); + ruleMap.remove(DataPermRuleType.TYPE_DEPT_USERS); + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + List userList = sysUserService.getSysUserList(filter, null); + Set userIdSet = userList.stream().map(SysUser::getUserId).collect(Collectors.toSet()); + resultMap.put(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS, CollUtil.join(userIdSet, ",")); + } + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_USERS) != null) { + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + List userList = sysUserService.getListByFilter(filter); + Set userIdSet = userList.stream().map(SysUser::getUserId).collect(Collectors.toSet()); + // 合并仅当前用户 + ruleMap.remove(DataPermRuleType.TYPE_USER_ONLY); + resultMap.put(DataPermRuleType.TYPE_DEPT_USERS, CollUtil.join(userIdSet, ",")); + } + if (ruleMap.get(DataPermRuleType.TYPE_USER_ONLY) != null) { + resultMap.put(DataPermRuleType.TYPE_USER_ONLY, "null"); + } + return resultMap; + } + + private String processMultiDeptAndChildren(Map> ruleMap, Long deptId) { + List parentDeptList = ruleMap.get(DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT); + if (parentDeptList == null) { + return null; + } + Set deptIdSet = new HashSet<>(); + for (SysDataPerm parentDept : parentDeptList) { + deptIdSet.addAll(StrUtil.split(parentDept.getDeptIdListString(), ',') + .stream().map(Long::valueOf).collect(Collectors.toSet())); + } + // 在合并所有的多父部门Id之后,需要判断是否有本部门及子部门的规则。如果有,就继续合并。 + if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) { + // 如果多父部门列表中包含当前部门,那么可以直接删除该规则了,如果没包含,就加入到多部门的DEPT_ID的IN LIST中。 + deptIdSet.add(deptId); + ruleMap.remove(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT); + } + // 需要与仅仅当前部门规则进行合并。 + if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_ONLY) && deptIdSet.contains(deptId)) { + ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY); + } + return StrUtil.join(",", deptIdSet); + } + + private String processMultiDept(Map> ruleMap, Long deptId) { + List customDeptList = ruleMap.get(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST); + if (customDeptList == null) { + return null; + } + Set deptIdSet = new HashSet<>(); + for (SysDataPerm customDept : customDeptList) { + deptIdSet.addAll(StrUtil.split(customDept.getDeptIdListString(), ',') + .stream().map(Long::valueOf).collect(Collectors.toSet())); + } + if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_ONLY)) { + deptIdSet.add(deptId); + ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY); + } + return StrUtil.join(",", deptIdSet); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addDataPermUserList(Long dataPermId, Set userIdSet) { + for (Long userId : userIdSet) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(userId); + sysDataPermUserMapper.insert(dataPermUser); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeDataPermUser(Long dataPermId, Long userId) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(userId); + return sysDataPermUserMapper.deleteByQuery(QueryWrapper.create(dataPermUser)) == 1; + } + + @Override + public CallResult verifyRelatedData(SysDataPerm dataPerm, String deptIdListString, String menuIdListString) { + JSONObject jsonObject = new JSONObject(); + if (dataPerm.getRuleType() == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT + || dataPerm.getRuleType() == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) { + if (StrUtil.isBlank(deptIdListString)) { + return CallResult.error("数据验证失败,部门列表不能为空!"); + } + Set deptIdSet = StrUtil.split( + deptIdListString, ",").stream().map(Long::valueOf).collect(Collectors.toSet()); + if (!sysDeptService.existAllPrimaryKeys(deptIdSet)) { + return CallResult.error("数据验证失败,存在不合法的部门数据,请刷新后重试!"); + } + jsonObject.put("deptIdSet", deptIdSet); + } + if (StrUtil.isNotBlank(menuIdListString)) { + Set menuIdSet = StrUtil.split( + menuIdListString, ",").stream().map(Long::valueOf).collect(Collectors.toSet()); + if (!sysMenuService.existAllPrimaryKeys(menuIdSet)) { + return CallResult.error("数据验证失败,存在不合法的菜单数据,请刷新后重试!"); + } + jsonObject.put("menuIdSet", menuIdSet); + } + return CallResult.ok(jsonObject); + } + + private void insertRelationData(SysDataPerm dataPerm, Set deptIdSet, Set menuIdSet) { + if (CollUtil.isNotEmpty(deptIdSet)) { + for (Long deptId : deptIdSet) { + SysDataPermDept dataPermDept = new SysDataPermDept(); + dataPermDept.setDataPermId(dataPerm.getDataPermId()); + dataPermDept.setDeptId(deptId); + sysDataPermDeptMapper.insert(dataPermDept); + } + } + if (CollUtil.isNotEmpty(menuIdSet)) { + for (Long menuId : menuIdSet) { + SysDataPermMenu dataPermMenu = new SysDataPermMenu(); + dataPermMenu.setDataPermId(dataPerm.getDataPermId()); + dataPermMenu.setMenuId(menuId); + sysDataPermMenuMapper.insert(dataPermMenu); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java new file mode 100644 index 00000000..3f30f41d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,312 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ObjectUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.webadmin.upms.dao.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 部门管理数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysDeptService") +public class SysDeptServiceImpl extends BaseService implements SysDeptService, BizWidgetDatasource { + + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private SysDeptMapper sysDeptMapper; + @Autowired + private SysDeptRelationMapper sysDeptRelationMapper; + @Autowired + private SysUserService sysUserService; + @Autowired + private SysDeptPostMapper sysDeptPostMapper; + @Autowired + private SysDataPermDeptMapper sysDataPermDeptMapper; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysDeptMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_DEPT_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysDept.class); + SysDept deptFilter = filter == null ? null : BeanUtil.toBean(filter, SysDept.class); + List deptList = this.getSysDeptList(deptFilter, orderBy); + this.buildRelationForDataList(deptList, MyRelationParam.dictOnly()); + return MyPageUtil.makeResponseData(deptList, BeanUtil::beanToMap); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List deptList; + if (StrUtil.isBlank(fieldName)) { + deptList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + deptList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysDept.class, fieldName, fieldValues)); + } + this.buildRelationForDataList(deptList, MyRelationParam.dictOnly()); + return MyModelUtil.beanToMapList(deptList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysDept saveNew(SysDept sysDept, SysDept parentSysDept) { + sysDept.setDeptId(idGenerator.nextLongId()); + sysDept.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.fillCommonsForInsert(sysDept); + sysDeptMapper.insert(sysDept); + // 同步插入部门关联关系数据 + if (parentSysDept == null) { + sysDeptRelationMapper.insert(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId())); + } else { + sysDeptRelationMapper.insertParentList(parentSysDept.getDeptId(), sysDept.getDeptId()); + } + return sysDept; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysDept sysDept, SysDept originalSysDept) { + MyModelUtil.fillCommonsForUpdate(sysDept, originalSysDept); + sysDept.setDeletedFlag(GlobalDeletedFlag.NORMAL); + if (sysDeptMapper.update(sysDept, false) == 0) { + return false; + } + if (ObjectUtil.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) { + this.updateParentRelation(sysDept, originalSysDept); + } + return true; + } + + private void updateParentRelation(SysDept sysDept, SysDept originalSysDept) { + List originalParentIdList = null; + // 1. 因为层级关系变化了,所以要先遍历出,当前部门的原有父部门Id列表。 + if (originalSysDept.getParentId() != null) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getDeptId()); + List relationList = sysDeptRelationMapper.selectListByQuery(queryWrapper); + originalParentIdList = relationList.stream() + .filter(c -> !c.getParentDeptId().equals(sysDept.getDeptId())) + .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList()); + } + // 2. 毕竟当前部门的上级部门变化了,所以当前部门和他的所有子部门,与当前部门的原有所有上级部门 + // 之间的关联关系就要被移除。 + // 这里先移除当前部门的所有子部门,与当前部门的所有原有上级部门之间的关联关系。 + if (CollUtil.isNotEmpty(originalParentIdList)) { + sysDeptRelationMapper.removeBetweenChildrenAndParents(originalParentIdList, sysDept.getDeptId()); + } + // 这里更进一步,将当前部门Id与其原有所有上级部门Id之间的关联关系删除。 + SysDeptRelation filter = new SysDeptRelation(); + filter.setDeptId(sysDept.getDeptId()); + sysDeptRelationMapper.deleteByQuery(QueryWrapper.create(filter)); + // 3. 重新计算当前部门的新上级部门列表。 + List newParentIdList = new LinkedList<>(); + // 这里要重新计算出当前部门所有新的上级部门Id列表。 + if (sysDept.getParentId() != null) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getParentId()); + List relationList = sysDeptRelationMapper.selectListByQuery(queryWrapper); + newParentIdList = relationList.stream() + .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList()); + } + // 4. 先查询出当前部门的所有下级子部门Id列表。 + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(SysDeptRelation::getParentDeptId, sysDept.getDeptId()); + List childRelationList = sysDeptRelationMapper.selectListByQuery(queryWrapper); + // 5. 将当前部门及其所有子部门Id与其新的所有上级部门Id之间,建立关联关系。 + List deptRelationList = new LinkedList<>(); + deptRelationList.add(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId())); + for (Long newParentId : newParentIdList) { + deptRelationList.add(new SysDeptRelation(newParentId, sysDept.getDeptId())); + for (SysDeptRelation childDeptRelation : childRelationList) { + deptRelationList.add(new SysDeptRelation(newParentId, childDeptRelation.getDeptId())); + } + } + // 6. 执行批量插入SQL语句,插入当前部门Id及其所有下级子部门Id,与所有新上级部门Id之间的关联关系。 + sysDeptRelationMapper.insertList(deptRelationList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long deptId) { + if (sysDeptMapper.deleteById(deptId) == 0) { + return false; + } + // 这里删除当前部门及其父部门的关联关系。 + // 当前部门和子部门的关系无需在这里删除,因为包含子部门时不能删除父部门。 + SysDeptRelation deptRelation = new SysDeptRelation(); + deptRelation.setDeptId(deptId); + sysDeptRelationMapper.deleteByQuery(QueryWrapper.create(deptRelation)); + SysDataPermDept dataPermDept = new SysDataPermDept(); + dataPermDept.setDeptId(deptId); + sysDataPermDeptMapper.deleteByQuery(QueryWrapper.create(dataPermDept)); + return true; + } + + @Override + public List getSysDeptList(SysDept filter, String orderBy) { + return sysDeptMapper.getSysDeptList(filter, orderBy); + } + + @Override + public List getSysDeptListWithRelation(SysDept filter, String orderBy) { + List resultList = sysDeptMapper.getSysDeptList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public boolean hasChildren(Long deptId) { + SysDept filter = new SysDept(); + filter.setParentId(deptId); + return getCountByFilter(filter) > 0; + } + + @Override + public boolean hasChildrenUser(Long deptId) { + SysUser sysUser = new SysUser(); + sysUser.setDeptId(deptId); + return sysUserService.getCountByFilter(sysUser) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addSysDeptPostList(List sysDeptPostList, Long deptId) { + for (SysDeptPost sysDeptPost : sysDeptPostList) { + sysDeptPost.setDeptPostId(idGenerator.nextLongId()); + sysDeptPost.setDeptId(deptId); + sysDeptPostMapper.insert(sysDeptPost); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateSysDeptPost(SysDeptPost sysDeptPost) { + SysDeptPost filter = new SysDeptPost(); + filter.setDeptPostId(sysDeptPost.getDeptPostId()); + filter.setDeptId(sysDeptPost.getDeptId()); + filter.setPostId(sysDeptPost.getPostId()); + return sysDeptPostMapper.updateByQuery(sysDeptPost, false, QueryWrapper.create(filter)) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeSysDeptPost(Long deptId, Long postId) { + SysDeptPost filter = new SysDeptPost(); + filter.setDeptId(deptId); + filter.setPostId(postId); + return sysDeptPostMapper.deleteByQuery(QueryWrapper.create(filter)) > 0; + } + + @Override + public SysDeptPost getSysDeptPost(Long deptId, Long postId) { + SysDeptPost filter = new SysDeptPost(); + filter.setDeptId(deptId); + filter.setPostId(postId); + return sysDeptPostMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + @Override + public SysDeptPost getSysDeptPost(Long deptPostId) { + return sysDeptPostMapper.selectOneById(deptPostId); + } + + @Override + public List> getSysDeptPostListWithRelationByDeptId(Long deptId) { + return sysDeptPostMapper.getSysDeptPostListWithRelationByDeptId(deptId); + } + + @Override + public List getSysDeptPostList(Long deptId, Set postIdSet) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(SysDeptPost::getDeptId, deptId); + queryWrapper.in(SysDeptPost::getPostId, postIdSet); + return sysDeptPostMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getSiblingSysDeptPostList(Long deptId, Set postIdSet) { + SysDept sysDept = this.getById(deptId); + if (sysDept == null) { + return new LinkedList<>(); + } + List deptList = this.getListByParentId("parentId", sysDept.getParentId()); + Set deptIdSet = deptList.stream().map(SysDept::getDeptId).collect(Collectors.toSet()); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(SysDeptPost::getDeptId, deptIdSet); + queryWrapper.in(SysDeptPost::getPostId, postIdSet); + return sysDeptPostMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getLeaderDeptPostIdList(Long deptId) { + List resultList = sysDeptPostMapper.getLeaderDeptPostList(deptId); + return resultList.stream().map(SysDeptPost::getDeptPostId).collect(Collectors.toList()); + } + + @Override + public List getUpLeaderDeptPostIdList(Long deptId) { + SysDept sysDept = this.getById(deptId); + if (sysDept.getParentId() == null) { + return new LinkedList<>(); + } + return this.getLeaderDeptPostIdList(sysDept.getParentId()); + } + + @Override + public List getAllChildDeptIdByParentIds(List parentIds) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(SysDeptRelation::getParentDeptId, parentIds); + return sysDeptRelationMapper.selectListByQuery(queryWrapper) + .stream().map(SysDeptRelation::getDeptId).collect(Collectors.toList()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java new file mode 100644 index 00000000..de7f99d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,233 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.webadmin.upms.bo.SysMenuExtraData; +import com.orangeforms.webadmin.upms.dao.SysMenuMapper; +import com.orangeforms.webadmin.upms.dao.SysRoleMenuMapper; +import com.orangeforms.webadmin.upms.model.SysMenu; +import com.orangeforms.webadmin.upms.model.SysRoleMenu; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import com.orangeforms.webadmin.upms.model.constant.SysOnlineMenuPermType; +import com.orangeforms.webadmin.upms.service.SysMenuService; +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.*; +import java.util.stream.Collectors; + +/** + * 菜单数据服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysMenuService") +public class SysMenuServiceImpl extends BaseService implements SysMenuService { + + @Autowired + private SysMenuMapper sysMenuMapper; + @Autowired + private SysRoleMenuMapper sysRoleMenuMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysMenuMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysMenu saveNew(SysMenu sysMenu) { + sysMenu.setMenuId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(sysMenu); + sysMenuMapper.insert(sysMenu); + // 判断当前菜单是否为指向在线表单的菜单,并将根据约定,动态插入两个子菜单。 + if (sysMenu.getOnlineFormId() != null && sysMenu.getOnlineFlowEntryId() == null) { + SysMenu viewSubMenu = new SysMenu(); + viewSubMenu.setMenuId(idGenerator.nextLongId()); + viewSubMenu.setParentId(sysMenu.getMenuId()); + viewSubMenu.setMenuType(SysMenuType.TYPE_BUTTON); + viewSubMenu.setMenuName("查看"); + viewSubMenu.setShowOrder(0); + viewSubMenu.setOnlineFormId(sysMenu.getOnlineFormId()); + viewSubMenu.setOnlineMenuPermType(SysOnlineMenuPermType.TYPE_VIEW); + MyModelUtil.fillCommonsForInsert(viewSubMenu); + sysMenuMapper.insert(viewSubMenu); + SysMenu editSubMenu = new SysMenu(); + editSubMenu.setMenuId(idGenerator.nextLongId()); + editSubMenu.setParentId(sysMenu.getMenuId()); + editSubMenu.setMenuType(SysMenuType.TYPE_BUTTON); + editSubMenu.setMenuName("编辑"); + editSubMenu.setShowOrder(1); + editSubMenu.setOnlineFormId(sysMenu.getOnlineFormId()); + editSubMenu.setOnlineMenuPermType(SysOnlineMenuPermType.TYPE_EDIT); + MyModelUtil.fillCommonsForInsert(editSubMenu); + sysMenuMapper.insert(editSubMenu); + } + return sysMenu; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysMenu sysMenu, SysMenu originalSysMenu) { + MyModelUtil.fillCommonsForUpdate(sysMenu, originalSysMenu); + sysMenu.setMenuType(originalSysMenu.getMenuType()); + if (sysMenuMapper.update(sysMenu, false) != 1) { + return false; + } + // 如果当前菜单的在线表单Id变化了,就需要同步更新他的内置子菜单也同步更新。 + if (ObjectUtil.notEqual(originalSysMenu.getOnlineFormId(), sysMenu.getOnlineFormId())) { + SysMenu onlineSubMenu = new SysMenu(); + onlineSubMenu.setOnlineFormId(sysMenu.getOnlineFormId()); + sysMenuMapper.updateByQuery(onlineSubMenu, + new QueryWrapper().eq(SysMenu::getParentId, sysMenu.getMenuId())); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(SysMenu menu) { + Long menuId = menu.getMenuId(); + if (sysMenuMapper.deleteByQuery(new QueryWrapper().eq(SysMenu::getMenuId, menuId)) != 1) { + return false; + } + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.deleteByQuery(QueryWrapper.create(roleMenu)); + // 如果为指向在线表单的菜单,则连同删除子菜单 + if (menu.getOnlineFormId() != null) { + SysMenu filter = new SysMenu(); + filter.setParentId(menuId); + List childMenus = sysMenuMapper.selectListByQuery(QueryWrapper.create(filter)); + sysMenuMapper.deleteByQuery(new QueryWrapper().eq(SysMenu::getParentId, menuId)); + if (CollUtil.isNotEmpty(childMenus)) { + List childMenuIds = childMenus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + sysRoleMenuMapper.deleteByQuery(new QueryWrapper().in(SysRoleMenu::getMenuId, childMenuIds)); + } + } + return true; + } + + @Override + public Collection getMenuListByUserId(Long userId) { + List menuList = sysMenuMapper.getMenuListByUserId(userId); + return this.distinctMenuList(menuList); + } + + @Override + public Collection getMenuListByRoleIds(String roleIds) { + if (StrUtil.isBlank(roleIds)) { + return CollUtil.empty(Long.class); + } + Set roleIdSet = StrUtil.split(roleIds, ",").stream().map(Long::valueOf).collect(Collectors.toSet()); + List menuList = sysMenuMapper.getMenuListByRoleIds(roleIdSet); + return this.distinctMenuList(menuList); + } + + @Override + public boolean hasChildren(Long menuId) { + SysMenu menu = new SysMenu(); + menu.setParentId(menuId); + return this.getCountByFilter(menu) > 0; + } + + @Override + public CallResult verifyRelatedData(SysMenu sysMenu, SysMenu originalSysMenu) { + // menu、ui fragment和button类型的menu不能没有parentId + if (sysMenu.getParentId() == null && sysMenu.getMenuType() != SysMenuType.TYPE_DIRECTORY) { + return CallResult.error("数据验证失败,当前类型菜单项的上级菜单不能为空!"); + } + if (this.needToVerify(sysMenu, originalSysMenu, SysMenu::getParentId)) { + String errorMessage = checkErrorOfNonDirectoryMenu(sysMenu); + if (errorMessage != null) { + return CallResult.error(errorMessage); + } + } + if (!this.verifyMenuCode(sysMenu, originalSysMenu)) { + return CallResult.error("数据验证失败,菜单编码已存在,不能重复使用!"); + } + return CallResult.ok(); + } + + @Override + public List getAllOnlineMenuList(Integer menuType) { + QueryWrapper queryWrapper = new QueryWrapper().isNotNull(SysMenu::getOnlineFormId); + if (menuType != null) { + queryWrapper.eq(SysMenu::getMenuType, menuType); + } + return sysMenuMapper.selectListByQuery(queryWrapper); + } + + private boolean verifyMenuCode(SysMenu sysMenu, SysMenu originalSysMenu) { + if (sysMenu.getExtraData() == null) { + return true; + } + String menuCode = JSON.parseObject(sysMenu.getExtraData(), SysMenuExtraData.class).getMenuCode(); + if (StrUtil.isBlank(menuCode)) { + return true; + } + String originalMenuCode = ""; + if (originalSysMenu != null && originalSysMenu.getExtraData() != null) { + originalMenuCode = JSON.parseObject(originalSysMenu.getExtraData(), SysMenuExtraData.class).getMenuCode(); + } + return StrUtil.equals(menuCode, originalMenuCode) + || sysMenuMapper.countMenuCode("\"menuCode\":\"" + menuCode + "\"") == 0; + } + + private String checkErrorOfNonDirectoryMenu(SysMenu sysMenu) { + // 判断父节点是否存在 + SysMenu parentSysMenu = getById(sysMenu.getParentId()); + if (parentSysMenu == null) { + return "数据验证失败,关联的上级菜单并不存在,请刷新后重试!"; + } + // 逐个判断每种类型的菜单,他的父菜单的合法性,先从目录类型和菜单类型开始 + if (sysMenu.getMenuType() == SysMenuType.TYPE_DIRECTORY + || sysMenu.getMenuType() == SysMenuType.TYPE_MENU) { + // 他们的上级只能是目录 + if (parentSysMenu.getMenuType() != SysMenuType.TYPE_DIRECTORY) { + return "数据验证失败,当前类型菜单项的上级菜单只能是目录类型!"; + } + } else if (sysMenu.getMenuType() == SysMenuType.TYPE_UI_FRAGMENT) { + // ui fragment的上级只能是menu类型 + if (parentSysMenu.getMenuType() != SysMenuType.TYPE_MENU) { + return "数据验证失败,当前类型菜单项的上级菜单只能是菜单类型和按钮类型!"; + } + } else if (sysMenu.getMenuType() == SysMenuType.TYPE_BUTTON) { + // button的上级只能是menu和ui fragment + if (parentSysMenu.getMenuType() != SysMenuType.TYPE_MENU + && parentSysMenu.getMenuType() != SysMenuType.TYPE_UI_FRAGMENT) { + return "数据验证失败,当前类型菜单项的上级菜单只能是菜单类型和UI片段类型!"; + } + } else { + return "数据验证失败,不支持的菜单类型!"; + } + return null; + } + + private Collection distinctMenuList(List menuList) { + LinkedHashMap menuMap = new LinkedHashMap<>(); + for (SysMenu menu : menuList) { + menuMap.put(menu.getMenuId(), menu); + } + return menuMap.values(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java new file mode 100644 index 00000000..69c4abb6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.dao.SysPermWhitelistMapper; +import com.orangeforms.webadmin.upms.model.SysPermWhitelist; +import com.orangeforms.webadmin.upms.service.SysPermWhitelistService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 权限资源白名单数据服务类。 + * 白名单中的权限资源,可以不受权限控制,任何用户皆可访问,一般用于常用的字典数据列表接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysPermWhitelistService") +public class SysPermWhitelistServiceImpl extends BaseService implements SysPermWhitelistService { + + @Autowired + private SysPermWhitelistMapper sysPermWhitelistMapper; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysPermWhitelistMapper; + } + + @Override + public List getWhitelistPermList() { + List dataList = this.getAllList(); + Function getterFunc = SysPermWhitelist::getPermUrl; + return dataList.stream() + .filter(x -> getterFunc.apply(x) != null).map(getterFunc).collect(Collectors.toList()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java new file mode 100644 index 00000000..67ce78a7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java @@ -0,0 +1,177 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.webadmin.upms.dao.SysDeptPostMapper; +import com.orangeforms.webadmin.upms.dao.SysPostMapper; +import com.orangeforms.webadmin.upms.dao.SysUserPostMapper; +import com.orangeforms.webadmin.upms.model.SysDeptPost; +import com.orangeforms.webadmin.upms.model.SysPost; +import com.orangeforms.webadmin.upms.model.SysUserPost; +import com.orangeforms.webadmin.upms.service.SysDeptService; +import com.orangeforms.webadmin.upms.service.SysPostService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 岗位管理数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysPostService") +public class SysPostServiceImpl extends BaseService implements SysPostService, BizWidgetDatasource { + + @Autowired + private SysPostMapper sysPostMapper; + @Autowired + private SysUserPostMapper sysUserPostMapper; + @Autowired + private SysDeptPostMapper sysDeptPostMapper; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysPostMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_POST_TYPE, this); + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_DEPT_POST_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysPost.class); + SysPost postFilter = filter == null ? null : BeanUtil.toBean(filter, SysPost.class); + if (StrUtil.equals(type, BizWidgetDatasourceType.UPMS_POST_TYPE)) { + List postList = this.getSysPostList(postFilter, orderBy); + return MyPageUtil.makeResponseData(postList, BeanUtil::beanToMap); + } + Assert.notNull(filter, "filter can't be NULL."); + Long deptId = (Long) filter.get("deptId"); + List> dataList = sysDeptService.getSysDeptPostListWithRelationByDeptId(deptId); + return MyPageUtil.makeResponseData(dataList); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List postList; + if (StrUtil.isBlank(fieldName)) { + postList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + postList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysPost.class, fieldName, fieldValues)); + } + return MyModelUtil.beanToMapList(postList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysPost saveNew(SysPost sysPost) { + sysPost.setPostId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(sysPost); + MyModelUtil.setDefaultValue(sysPost, "leaderPost", false); + sysPostMapper.insert(sysPost); + return sysPost; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysPost sysPost, SysPost originalSysPost) { + MyModelUtil.fillCommonsForUpdate(sysPost, originalSysPost); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return sysPostMapper.update(sysPost, false) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long postId) { + if (sysPostMapper.deleteById(postId) != 1) { + return false; + } + // 开始删除多对多父表的关联 + sysUserPostMapper.deleteByQuery(new QueryWrapper().eq(SysUserPost::getPostId, postId)); + sysDeptPostMapper.deleteByQuery(new QueryWrapper().eq(SysDeptPost::getPostId, postId)); + return true; + } + + @Override + public List getSysPostList(SysPost filter, String orderBy) { + return sysPostMapper.getSysPostList(filter, orderBy); + } + + @Override + public List getSysPostListWithRelation(SysPost filter, String orderBy) { + List resultList = sysPostMapper.getSysPostList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public List getNotInSysPostListByDeptId(Long deptId, SysPost filter, String orderBy) { + List resultList = sysPostMapper.getNotInSysPostListByDeptId(deptId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + @Override + public List getSysPostListByDeptId(Long deptId, SysPost filter, String orderBy) { + List resultList = sysPostMapper.getSysPostListByDeptId(deptId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + @Override + public List getSysUserPostListByUserId(Long userId) { + return sysUserPostMapper.selectListByQuery(new QueryWrapper().eq(SysUserPost::getUserId, userId)); + } + + @Override + public boolean existAllPrimaryKeys(Set deptPostIdSet, Long deptId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(SysDeptPost::getDeptId, deptId); + queryWrapper.in(SysDeptPost::getDeptPostId, deptPostIdSet); + return sysDeptPostMapper.selectCountByQuery(queryWrapper) == deptPostIdSet.size(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java new file mode 100644 index 00000000..fc0d25b0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,188 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.webadmin.upms.dao.SysRoleMapper; +import com.orangeforms.webadmin.upms.dao.SysRoleMenuMapper; +import com.orangeforms.webadmin.upms.dao.SysUserRoleMapper; +import com.orangeforms.webadmin.upms.model.SysRole; +import com.orangeforms.webadmin.upms.model.SysRoleMenu; +import com.orangeforms.webadmin.upms.model.SysUserRole; +import com.orangeforms.webadmin.upms.service.SysMenuService; +import com.orangeforms.webadmin.upms.service.SysRoleService; +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 jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 角色数据服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysRoleService") +public class SysRoleServiceImpl extends BaseService implements SysRoleService, BizWidgetDatasource { + + @Autowired + private SysRoleMapper sysRoleMapper; + @Autowired + private SysRoleMenuMapper sysRoleMenuMapper; + @Autowired + private SysUserRoleMapper sysUserRoleMapper; + @Autowired + private SysMenuService sysMenuService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysRoleMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_ROLE_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysRole.class); + SysRole roleFilter = filter == null ? null : BeanUtil.toBean(filter, SysRole.class); + List roleList = this.getSysRoleList(roleFilter, orderBy); + return MyPageUtil.makeResponseData(roleList, BeanUtil::beanToMap); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List roleList; + if (StrUtil.isBlank(fieldName)) { + roleList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + roleList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysRole.class, fieldName, fieldValues)); + } + return MyModelUtil.beanToMapList(roleList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysRole saveNew(SysRole role, Set menuIdSet) { + role.setRoleId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(role); + sysRoleMapper.insert(role); + if (menuIdSet != null) { + for (Long menuId : menuIdSet) { + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setRoleId(role.getRoleId()); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.insert(roleMenu); + } + } + return role; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysRole role, SysRole originalRole, Set menuIdSet) { + MyModelUtil.fillCommonsForUpdate(role, originalRole); + if (sysRoleMapper.update(role) != 1) { + return false; + } + SysRoleMenu deletedRoleMenu = new SysRoleMenu(); + deletedRoleMenu.setRoleId(role.getRoleId()); + sysRoleMenuMapper.deleteByQuery(QueryWrapper.create(deletedRoleMenu)); + if (menuIdSet != null) { + for (Long menuId : menuIdSet) { + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setRoleId(role.getRoleId()); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.insert(roleMenu); + } + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long roleId) { + if (sysRoleMapper.deleteById(roleId) != 1) { + return false; + } + sysRoleMenuMapper.deleteByQuery(new QueryWrapper().eq(SysRoleMenu::getRoleId, roleId)); + sysUserRoleMapper.deleteByQuery(new QueryWrapper().eq(SysUserRole::getRoleId, roleId)); + return true; + } + + @Override + public List getSysRoleList(SysRole filter, String orderBy) { + return sysRoleMapper.getSysRoleList(filter, orderBy); + } + + @Override + public List getSysUserRoleListByUserId(Long userId) { + SysUserRole filter = new SysUserRole(); + filter.setUserId(userId); + return sysUserRoleMapper.selectListByQuery(new QueryWrapper().eq(SysUserRole::getUserId, userId)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addUserRoleList(List userRoleList) { + for (SysUserRole userRole : userRoleList) { + sysUserRoleMapper.insert(userRole); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeUserRole(Long roleId, Long userId) { + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(roleId); + userRole.setUserId(userId); + return sysUserRoleMapper.deleteByQuery(QueryWrapper.create(userRole)) == 1; + } + + @Override + public CallResult verifyRelatedData(SysRole sysRole, SysRole originalSysRole, String menuIdListString) { + JSONObject jsonObject = null; + if (StringUtils.isNotBlank(menuIdListString)) { + Set menuIdSet = Arrays.stream( + menuIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysMenuService.existAllPrimaryKeys(menuIdSet)) { + return CallResult.error("数据验证失败,存在不合法的菜单权限,请刷新后重试!"); + } + jsonObject = new JSONObject(); + jsonObject.put("menuIdSet", menuIdSet); + } + return CallResult.ok(jsonObject); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java new file mode 100644 index 00000000..d4ccbd20 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java @@ -0,0 +1,383 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.webadmin.upms.dao.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.constant.UserFilterGroup; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 用户管理数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysUserService") +public class SysUserServiceImpl extends BaseService implements SysUserService, BizWidgetDatasource { + + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private SysUserMapper sysUserMapper; + @Autowired + private SysUserRoleMapper sysUserRoleMapper; + @Autowired + private SysUserPostMapper sysUserPostMapper; + @Autowired + private SysDataPermUserMapper sysDataPermUserMapper; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysRoleService sysRoleService; + @Autowired + private SysDataPermService sysDataPermService; + @Autowired + private SysPostService sysPostService; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysUserMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_USER_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + List userList = null; + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class, false); + SysUser userFilter = BeanUtil.toBean(filter, SysUser.class); + if (filter != null) { + Object group = filter.get("USER_FILTER_GROUP"); + if (group != null) { + JSONObject filterGroupJson = JSON.parseObject(group.toString()); + String groupType = filterGroupJson.getString("type"); + String values = filterGroupJson.getString("values"); + if (UserFilterGroup.USER.equals(groupType)) { + List loginNames = StrUtil.splitTrim(values, ","); + userList = sysUserMapper.getSysUserListByLoginNames(loginNames, userFilter, orderBy); + } else { + Set groupIds = StrUtil.splitTrim(values, ",") + .stream().map(Long::valueOf).collect(Collectors.toSet()); + userList = this.getUserListByGroupIds(groupType, groupIds, userFilter, orderBy); + } + } + } + if (userList == null) { + userList = this.getSysUserList(userFilter, orderBy); + } + this.buildRelationForDataList(userList, MyRelationParam.dictOnly()); + return MyPageUtil.makeResponseData(userList, BeanUtil::beanToMap); + } + + private List getUserListByGroupIds(String groupType, Set groupIds, SysUser filter, String orderBy) { + if (groupType.equals(UserFilterGroup.DEPT)) { + return sysUserMapper.getSysUserListByDeptIds(groupIds, filter, orderBy); + } + List userIds = null; + switch (groupType) { + case UserFilterGroup.ROLE: + userIds = sysUserMapper.getUserIdListByRoleIds(groupIds, filter, orderBy); + break; + case UserFilterGroup.POST: + userIds = sysUserMapper.getUserIdListByPostIds(groupIds, filter, orderBy); + break; + case UserFilterGroup.DEPT_POST: + userIds = sysUserMapper.getUserIdListByDeptPostIds(groupIds, filter, orderBy); + break; + default: + break; + } + if (CollUtil.isEmpty(userIds)) { + return CollUtil.empty(SysUser.class); + } + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(SysUser::getUserId, userIds); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return sysUserMapper.selectListByQuery(queryWrapper); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List userList; + if (StrUtil.isBlank(fieldName)) { + userList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + userList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysUser.class, fieldName, fieldValues)); + } + this.buildRelationForDataList(userList, MyRelationParam.dictOnly()); + return MyModelUtil.beanToMapList(userList); + } + + /** + * 获取指定登录名的用户对象。 + * + * @param loginName 指定登录用户名。 + * @return 用户对象。 + */ + @Override + public SysUser getSysUserByLoginName(String loginName) { + SysUser filter = new SysUser(); + filter.setLoginName(loginName); + return sysUserMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysUser saveNew(SysUser user, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet) { + user.setUserId(idGenerator.nextLongId()); + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setUserStatus(SysUserStatus.STATUS_NORMAL); + user.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.fillCommonsForInsert(user); + sysUserMapper.insert(user); + if (CollUtil.isNotEmpty(deptPostIdSet)) { + for (Long deptPostId : deptPostIdSet) { + SysDeptPost deptPost = sysDeptService.getSysDeptPost(deptPostId); + SysUserPost userPost = new SysUserPost(); + userPost.setUserId(user.getUserId()); + userPost.setDeptPostId(deptPostId); + userPost.setPostId(deptPost.getPostId()); + sysUserPostMapper.insert(userPost); + } + } + if (CollUtil.isNotEmpty(roleIdSet)) { + for (Long roleId : roleIdSet) { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(user.getUserId()); + userRole.setRoleId(roleId); + sysUserRoleMapper.insert(userRole); + } + } + if (CollUtil.isNotEmpty(dataPermIdSet)) { + for (Long dataPermId : dataPermIdSet) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(user.getUserId()); + sysDataPermUserMapper.insert(dataPermUser); + } + } + return user; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysUser user, SysUser originalUser, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet) { + user.setLoginName(originalUser.getLoginName()); + user.setPassword(originalUser.getPassword()); + user.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.fillCommonsForUpdate(user, originalUser); + if (sysUserMapper.update(user, false) != 1) { + return false; + } + // 先删除原有的User-Post关联关系,再重新插入新的关联关系 + SysUserPost deletedUserPost = new SysUserPost(); + deletedUserPost.setUserId(user.getUserId()); + sysUserPostMapper.deleteByQuery(QueryWrapper.create(deletedUserPost)); + if (CollUtil.isNotEmpty(deptPostIdSet)) { + for (Long deptPostId : deptPostIdSet) { + SysDeptPost deptPost = sysDeptService.getSysDeptPost(deptPostId); + SysUserPost userPost = new SysUserPost(); + userPost.setUserId(user.getUserId()); + userPost.setDeptPostId(deptPostId); + userPost.setPostId(deptPost.getPostId()); + sysUserPostMapper.insert(userPost); + } + } + // 先删除原有的User-Role关联关系,再重新插入新的关联关系 + SysUserRole deletedUserRole = new SysUserRole(); + deletedUserRole.setUserId(user.getUserId()); + sysUserRoleMapper.deleteByQuery(QueryWrapper.create(deletedUserRole)); + if (CollUtil.isNotEmpty(roleIdSet)) { + for (Long roleId : roleIdSet) { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(user.getUserId()); + userRole.setRoleId(roleId); + sysUserRoleMapper.insert(userRole); + } + } + // 先删除原有的DataPerm-User关联关系,在重新插入新的关联关系 + SysDataPermUser deletedDataPermUser = new SysDataPermUser(); + deletedDataPermUser.setUserId(user.getUserId()); + sysDataPermUserMapper.deleteByQuery(QueryWrapper.create(deletedDataPermUser)); + if (CollUtil.isNotEmpty(dataPermIdSet)) { + for (Long dataPermId : dataPermIdSet) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(user.getUserId()); + sysDataPermUserMapper.insert(dataPermUser); + } + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean changePassword(Long userId, String newPass) { + SysUser updatedUser = new SysUser(); + updatedUser.setUserId(userId); + updatedUser.setPassword(passwordEncoder.encode(newPass)); + return sysUserMapper.update(updatedUser) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean changeHeadImage(Long userId, String newHeadImage) { + SysUser updatedUser = new SysUser(); + updatedUser.setUserId(userId); + updatedUser.setHeadImageUrl(newHeadImage); + return sysUserMapper.update(updatedUser) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long userId) { + if (sysUserMapper.deleteById(userId) == 0) { + return false; + } + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(userId); + sysUserRoleMapper.deleteByQuery(QueryWrapper.create(userRole)); + SysUserPost userPost = new SysUserPost(); + userPost.setUserId(userId); + sysUserPostMapper.deleteByQuery(QueryWrapper.create(userPost)); + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setUserId(userId); + sysDataPermUserMapper.deleteByQuery(QueryWrapper.create(dataPermUser)); + return true; + } + + @Override + public List getSysUserList(SysUser filter, String orderBy) { + return sysUserMapper.getSysUserList(filter, orderBy); + } + + @Override + public List getSysUserListWithRelation(SysUser filter, String orderBy) { + List resultList = sysUserMapper.getSysUserList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public List getSysUserListByRoleId(Long roleId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByRoleId(roleId, filter, orderBy); + } + + @Override + public List getNotInSysUserListByRoleId(Long roleId, SysUser filter, String orderBy) { + return sysUserMapper.getNotInSysUserListByRoleId(roleId, filter, orderBy); + } + + @Override + public List getSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByDataPermId(dataPermId, filter, orderBy); + } + + @Override + public List getNotInSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy) { + return sysUserMapper.getNotInSysUserListByDataPermId(dataPermId, filter, orderBy); + } + + @Override + public List getSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByDeptPostId(deptPostId, filter, orderBy); + } + + @Override + public List getNotInSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy) { + return sysUserMapper.getNotInSysUserListByDeptPostId(deptPostId, filter, orderBy); + } + + @Override + public List getSysUserListByPostId(Long postId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByPostId(postId, filter, orderBy); + } + + @Override + public CallResult verifyRelatedData( + SysUser sysUser, SysUser originalSysUser, String roleIds, String deptPostIds, String dataPermIds) { + JSONObject jsonObject = new JSONObject(); + if (StrUtil.isBlank(deptPostIds)) { + return CallResult.error("数据验证失败,用户的部门岗位数据不能为空!"); + } + Set deptPostIdSet = + Arrays.stream(deptPostIds.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysPostService.existAllPrimaryKeys(deptPostIdSet, sysUser.getDeptId())) { + return CallResult.error("数据验证失败,存在不合法的用户岗位,请刷新后重试!"); + } + jsonObject.put("deptPostIdSet", deptPostIdSet); + if (StrUtil.isBlank(roleIds)) { + return CallResult.error("数据验证失败,用户的角色数据不能为空!"); + } + Set roleIdSet = Arrays.stream( + roleIds.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysRoleService.existAllPrimaryKeys(roleIdSet)) { + return CallResult.error("数据验证失败,存在不合法的用户角色,请刷新后重试!"); + } + jsonObject.put("roleIdSet", roleIdSet); + if (StrUtil.isBlank(dataPermIds)) { + return CallResult.error("数据验证失败,用户的数据权限不能为空!"); + } + Set dataPermIdSet = Arrays.stream( + dataPermIds.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysDataPermService.existAllPrimaryKeys(dataPermIdSet)) { + return CallResult.error("数据验证失败,存在不合法的数据权限,请刷新后重试!"); + } + jsonObject.put("dataPermIdSet", dataPermIdSet); + //这里是基于字典的验证。 + if (this.needToVerify(sysUser, originalSysUser, SysUser::getDeptId) + && !sysDeptService.existId(sysUser.getDeptId())) { + return CallResult.error("数据验证失败,关联的用户部门Id并不存在,请刷新后重试!"); + } + return CallResult.ok(jsonObject); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java new file mode 100644 index 00000000..601dc7c2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与部门关联VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与部门关联VO") +@Data +public class SysDataPermDeptVo { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id") + private Long dataPermId; + + /** + * 关联部门Id。 + */ + @Schema(description = "关联部门Id") + private Long deptId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java new file mode 100644 index 00000000..7e4bc12c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与菜单关联VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与菜单关联VO") +@Data +public class SysDataPermMenuVo { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id") + private Long dataPermId; + + /** + * 关联菜单Id。 + */ + @Schema(description = "关联菜单Id") + private Long menuId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java new file mode 100644 index 00000000..e07af624 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java @@ -0,0 +1,57 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +/** + * 数据权限VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysDataPermVo extends BaseVo { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id") + private Long dataPermId; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称") + private String dataPermName; + + /** + * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。 + */ + @Schema(description = "数据权限规则类型") + private Integer ruleType; + + /** + * 部门Id列表(逗号分隔)。 + */ + @Schema(description = "部门Id列表") + private String deptIdListString; + + /** + * 数据权限与部门关联对象列表。 + */ + @Schema(description = "数据权限与部门关联对象列表") + private List> dataPermDeptList; + + /** + * 数据权限与菜单关联对象列表。 + */ + @Schema(description = "数据权限与菜单关联对象列表") + private List> dataPermMenuList; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java new file mode 100644 index 00000000..6e502095 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 部门岗位VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "部门岗位VO") +@Data +public class SysDeptPostVo { + + /** + * 部门岗位Id。 + */ + @Schema(description = "部门岗位Id") + private Long deptPostId; + + /** + * 部门Id。 + */ + @Schema(description = "部门Id") + private Long deptId; + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id") + private Long postId; + + /** + * 部门岗位显示名称。 + */ + @Schema(description = "部门岗位显示名称") + private String postShowName; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java new file mode 100644 index 00000000..1f08901f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java @@ -0,0 +1,65 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 部门管理VO视图对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysDeptVO视图对象") +@Data +public class SysDeptVo { + + /** + * 部门Id。 + */ + @Schema(description = "部门Id") + private Long deptId; + + /** + * 部门名称。 + */ + @Schema(description = "部门名称") + private String deptName; + + /** + * 显示顺序。 + */ + @Schema(description = "显示顺序") + private Integer showOrder; + + /** + * 父部门Id。 + */ + @Schema(description = "父部门Id") + private Long parentId; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java new file mode 100644 index 00000000..e278c859 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java @@ -0,0 +1,90 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "菜单VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysMenuVo extends BaseVo { + + /** + * 菜单Id。 + */ + @Schema(description = "菜单Id") + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null + */ + @Schema(description = "父菜单Id") + private Long parentId; + + /** + * 菜单显示名称。 + */ + @Schema(description = "菜单显示名称") + private String menuName; + + /** + * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + @Schema(description = "菜单类型") + private Integer menuType; + + /** + * 前端表单路由名称,仅用于menu_type为1的菜单类型。 + */ + @Schema(description = "前端表单路由名称") + private String formRouterName; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + @Schema(description = "在线表单主键Id") + private Long onlineFormId; + + /** + * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。 + */ + @Schema(description = "在线表单菜单的权限控制类型") + private Integer onlineMenuPermType; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + @Schema(description = "统计页面主键Id") + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + @Schema(description = "仅用于在线表单的流程Id") + private Long onlineFlowEntryId; + + /** + * 菜单显示顺序 (值越小,排序越靠前)。 + */ + @Schema(description = "菜单显示顺序") + private Integer showOrder; + + /** + * 菜单图标。 + */ + @Schema(description = "菜单显示图标") + private String icon; + + /** + * 附加信息。 + */ + @Schema(description = "附加信息") + private String extraData; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java new file mode 100644 index 00000000..15a5f2c7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java @@ -0,0 +1,50 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * 岗位VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "岗位VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysPostVo extends BaseVo { + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id") + private Long postId; + + /** + * 岗位名称。 + */ + @Schema(description = "岗位名称") + private String postName; + + /** + * 岗位层级,数值越小级别越高。 + */ + @Schema(description = "岗位层级,数值越小级别越高") + private Integer postLevel; + + /** + * 是否领导岗位。 + */ + @Schema(description = "是否领导岗位") + private Boolean leaderPost; + + /** + * postId 的多对多关联表数据对象,数据对应类型为SysDeptPostVo。 + */ + @Schema(description = "postId 的多对多关联表数据对象") + private Map sysDeptPost; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java new file mode 100644 index 00000000..0aaf0358 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +/** + * 角色VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "角色VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysRoleVo extends BaseVo { + + /** + * 角色Id。 + */ + @Schema(description = "角色Id") + private Long roleId; + + /** + * 角色名称。 + */ + @Schema(description = "角色名称") + private String roleName; + + /** + * 角色与菜单关联对象列表。 + */ + @Schema(description = "角色与菜单关联对象列表") + private List> sysRoleMenuList; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java new file mode 100644 index 00000000..194e8d86 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java @@ -0,0 +1,133 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; +import java.util.List; + +/** + * 用户管理VO视图对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysUserVO视图对象") +@Data +public class SysUserVo { + + /** + * 用户Id。 + */ + @Schema(description = "用户Id") + private Long userId; + + /** + * 登录用户名。 + */ + @Schema(description = "登录用户名") + private String loginName; + + /** + * 用户部门Id。 + */ + @Schema(description = "用户部门Id") + private Long deptId; + + /** + * 用户显示名称。 + */ + @Schema(description = "用户显示名称") + private String showName; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)") + private Integer userType; + + /** + * 用户头像的Url。 + */ + @Schema(description = "用户头像的Url") + private String headImageUrl; + + /** + * 用户状态(0: 正常 1: 锁定)。 + */ + @Schema(description = "用户状态(0: 正常 1: 锁定)") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + @Schema(description = "用户邮箱") + private String email; + + /** + * 用户手机。 + */ + @Schema(description = "用户手机") + private String mobile; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 多对多用户岗位数据集合。 + */ + @Schema(description = "多对多用户岗位数据集合") + private List> sysUserPostList; + + /** + * 多对多用户角色数据集合。 + */ + @Schema(description = "多对多用户角色数据集合") + private List> sysUserRoleList; + + /** + * 多对多用户数据权限数据集合。 + */ + @Schema(description = "多对多用户数据权限数据集合") + private List> sysDataPermUserList; + + /** + * deptId 字典关联数据。 + */ + @Schema(description = "deptId 字典关联数据") + private Map deptIdDictMap; + + /** + * userType 常量字典关联数据。 + */ + @Schema(description = "userType 常量字典关联数据") + private Map userTypeDictMap; + + /** + * userStatus 常量字典关联数据。 + */ + @Schema(description = "userStatus 常量字典关联数据") + private Map userStatusDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application-dev.yml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application-dev.yml new file mode 100644 index 00000000..47f2c1d5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application-dev.yml @@ -0,0 +1,171 @@ +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + druid: + # 数据库链接 [主数据源] + main: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + # 默认生成的操作日志数据源配置。 + operation-log: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + # 默认生成的全局编码字典数据源配置。 + global-dict: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + # 默认生成的工作流及在线表单数据源配置。 + common-flow-online: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + driverClassName: com.mysql.cj.jdbc.Driver + name: application-webadmin + initialSize: 10 + minIdle: 10 + maxActive: 50 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + maxOpenPreparedStatements: 20 + validationQuery: SELECT 'x' + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 + filters: stat,wall + useGlobalDataSourceStat: true + web-stat-filter: + enabled: true + url-pattern: /* + exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*,/actuator/*" + stat-view-servlet: + enabled: true + urlPattern: /druid/* + resetEnable: true + +application: + # 初始化密码。 + defaultUserPassword: 123456 + # 缺省的文件上传根目录。 + uploadFileBaseDir: ./zz-resource/upload-files/app + # 跨域的IP(http://192.168.10.10:8086)白名单列表,多个IP之间逗号分隔(* 表示全部信任,空白表示禁用跨域信任)。 + credentialIpList: "*" + # Session的用户和数据权限在Redis中的过期时间(秒)。一定要和sa-token.timeout + sessionExpiredSeconds: 86400 + # 是否排他登录。 + excludeLogin: false + +# 这里仅仅是一个第三方配置的示例,如果没有接入斯三方系统, +# 这里的配置项也不会影响到系统的行为,如果觉得多余,也可以手动删除。 +third-party: + # 第三方系统接入的用户鉴权配置。 + auth: + - appCode: orange-forms-default + # 访问第三方系统接口的URL前缀,橙单会根据功能添加接口路径的其余部分, + # 比如获取用户Token的接口 http://localhost:8083/orangePluginTest/getTokenData + baseUrl: http://localhost:8083/orangePlugin + # 第三方应用鉴权的HTTP请求令牌头的KEY。 + tokenHeaderKey: Authorization + # 第三方返回的用户Token数据的缓存过期时长,单位秒。 + # 如果为0,则不缓存,每次涉及第三方的请求,都会发出http请求,交由第三方验证,这样对系统性能会有影响。 + tokenExpiredSeconds: 60 + # 第三方返回的权限数据的缓存过期时长,单位秒。 + permExpiredSeconds: 86400 + +# 这里仅仅是一个第三方配置的示例,如果没有接入斯三方系统, +# 这里的配置项也不会影响到系统的行为,如果觉得多余,也可以手动删除。 +common-ext: + urlPrefix: /admin/commonext + # 这里可以配置多个第三方应用,这里的应用数量,通常会和上面third-party.auth的配置数量一致。 + apps: + # 应用唯一编码,尽量不要使用中文。 + - appCode: orange-forms-default + # 业务组件的数据源配置。 + bizWidgetDatasources: + # 组件的类型,多个类型之间可以逗号分隔。 + - types: upms_user,upms_dept + # 组件获取列表数据的接口地址。 + listUrl: http://localhost:8083/orangePlugin/listBizWidgetData + # 组件获取详情数据的接口地址。 + viewUrl: http://localhost:8083/orangePlugin/viewBizWidgetData + +common-sequence: + # Snowflake 分布式Id生成算法所需的WorkNode参数值。 + snowflakeWorkNode: 1 + +# 存储session数据的Redis,所有服务均需要,因此放到公共配置中。 +# 根据实际情况,该Redis也可以用于存储其他数据。 +common-redis: + # redisson的配置。每个服务可以自己的配置文件中覆盖此选项。 + redisson: + # 如果该值为false,系统将不会创建RedissionClient的bean。 + enabled: true + # mode的可用值为,single/cluster/sentinel/master-slave + mode: single + # single: 单机模式 + # address: redis://localhost:6379 + # cluster: 集群模式 + # 每个节点逗号分隔,同时每个节点前必须以redis://开头。 + # address: redis://localhost:6379,redis://localhost:6378,... + # sentinel: + # 每个节点逗号分隔,同时每个节点前必须以redis://开头。 + # address: redis://localhost:6379,redis://localhost:6378,... + # master-slave: + # 每个节点逗号分隔,第一个为主节点,其余为从节点。同时每个节点前必须以redis://开头。 + # address: redis://localhost:6379,redis://localhost:6378,... + address: redis://localhost:6379 + # 链接超时,单位毫秒。 + timeout: 6000 + # 单位毫秒。分布式锁的超时检测时长。 + # 如果一次锁内操作超该毫秒数,或在释放锁之前异常退出,Redis会在该时长之后主动删除该锁使用的key。 + lockWatchdogTimeout: 60000 + # redis 密码,空可以不填。 + password: + pool: + # 连接池数量。 + poolSize: 20 + # 连接池中最小空闲数量。 + minIdle: 5 + +minio: + enabled: false + endpoint: http://localhost:19000 + accessKey: admin + secretKey: admin123456 + bucketName: application + +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: Authorization + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: ${application.sessionExpiredSeconds} + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + # 配置 Sa-Token 单独使用的 Redis 连接 + alone-redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: localhost + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + is-read-header: true + is-read-cookie: false diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application.yml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application.yml new file mode 100644 index 00000000..b3bd45c1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/application.yml @@ -0,0 +1,164 @@ +logging: + level: + # 这里设置的日志级别优先于logback-spring.xml文件Loggers中的日志级别。 + com.orangeforms: info + config: classpath:logback-spring.xml + +server: + port: 8082 + tomcat: + uri-encoding: UTF-8 + threads: + max: 100 + min-spare: 10 + servlet: + encoding: + force: true + charset: UTF-8 + enabled: true + +# spring相关配置 +spring: + application: + name: application-webadmin + profiles: + active: dev + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB + mvc: + converters: + preferred-json-mapper: fastjson + main: + allow-circular-references: true + groovy: + template: + check-template-location: false + +flowable: + async-executor-activate: false + database-schema-update: false + +mybatis-flex: + mapper-locations: classpath:com/orangeforms/webadmin/*/dao/mapper/*Mapper.xml,com/orangeforms/common/log/dao/mapper/*Mapper.xml,com/orangeforms/common/online/dao/mapper/*Mapper.xml,com/orangeforms/common/flow/dao/mapper/*Mapper.xml + type-aliases-package: com.orangeforms.webadmin.*.model,com.orangeforms.common.log.model,com.orangeforms.common.online.model,com.orangeforms.common.flow.model + global-config: + deleted-value-of-logic-delete: -1 + normal-value-of-logic-delete: 1 + +# 自动分页的配置 +pagehelper: + helperDialect: mysql + reasonable: true + supportMethodsArguments: false + params: count=countSql + +common-core: + # 可选值为 mysql / postgresql / oracle / dm8 / kingbase / opengauss + databaseType: mysql + +common-online: + # 注意不要以反斜杠(/)结尾。 + urlPrefix: /admin/online + # 打印接口的路径,不要以反斜杠(/)结尾。 + printUrlPath: /admin/report/reportPrint/print + # 在线表单业务数据上传资源路径 + uploadFileBaseDir: ./zz-resource/upload-files/online + # 如果为false,在线表单模块中所有Controller接口将不能使用。 + operationEnabled: true + # 1: minio 2: aliyun-oss 3: qcloud-cos。 + distributeStoreType: 1 + # 调用render接口时候,是否打开一级缓存加速。 + enableRenderCache: false + # 业务表和在线表单内置表是否跨库。 + enabledMultiDatabaseWrite: true + # 脱敏字段的掩码字符,只能为单个字符。 + maskChar: '*' + # 下面的url列表,请保持反斜杠(/)结尾。 + viewUrlList: + - ${common-online.urlPrefix}/onlineOperation/viewByDatasourceId/ + - ${common-online.urlPrefix}/onlineOperation/viewByOneToManyRelationId/ + - ${common-online.urlPrefix}/onlineOperation/listByDatasourceId/ + - ${common-online.urlPrefix}/onlineOperation/listByOneToManyRelationId/ + - ${common-online.urlPrefix}/onlineOperation/exportByDatasourceId/ + - ${common-online.urlPrefix}/onlineOperation/exportByOneToManyRelationId/ + - ${common-online.urlPrefix}/onlineOperation/downloadDatasource/ + - ${common-online.urlPrefix}/onlineOperation/downloadOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/print/ + editUrlList: + - ${common-online.urlPrefix}/onlineOperation/addDatasource/ + - ${common-online.urlPrefix}/onlineOperation/addOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/updateDatasource/ + - ${common-online.urlPrefix}/onlineOperation/updateOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/deleteDatasource/ + - ${common-online.urlPrefix}/onlineOperation/deleteOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/deleteBatchDatasource/ + - ${common-online.urlPrefix}/onlineOperation/deleteBatchOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/uploadDatasource/ + - ${common-online.urlPrefix}/onlineOperation/uploadOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/importDatasource/ + +common-flow: + # 请慎重修改urlPrefix的缺省配置,注意不要以反斜杠(/)结尾。如必须修改其他路径,请同步修改数据库脚本。 + urlPrefix: /admin/flow + # 如果为false,流程模块的所有Controller中的接口将不能使用。 + operationEnabled: true + +common-swagger: + # 当enabled为false的时候,则可禁用swagger。 + enabled: true + # 工程的基础包名。 + basePackage: com.orangeforms + # 工程服务的基础包名。 + serviceBasePackage: com.orangeforms.webadmin + title: 橙单单体服务工程 + description: 橙单单体服务工程详情 + version: 1.0 + +springdoc: + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha + #operations-sorter: order + api-docs: + path: /v3/api-docs + default-flat-param-object: false + +common-datafilter: + tenant: + # 对于单体服务,该值始终为false。 + enabled: false + dataperm: + enabled: true + # 在拼接数据权限过滤的SQL时,我们会用到sys_dept_relation表,该表的前缀由此配置项指定。 + # 如果没有前缀,请使用 "" 。 + deptRelationTablePrefix: zz_ + # 是否在每次执行数据权限查询过滤时,都要进行菜单Id和URL之间的越权验证。如果使用SaToken权限框架,该参数必须为false。 + enableMenuPermVerify: false + +# 暴露监控端点 +management: + endpoints: + web: + exposure: + include: '*' + jmx: + exposure: + include: '*' + endpoint: + # 与中间件相关的健康详情也会被展示 + health: + show-details: always + configprops: + # 在/actuator/configprops中,所有包含password的配置,将用 * 隐藏。 + # 如果不想隐藏任何配置项的值,可以直接使用如下被注释的空值。 + # keys-to-sanitize: + keys-to-sanitize: password + server: + base-path: "/" + +common-log: + # 操作日志配置,对应配置文件common-log/OperationLogProperties.java + operation-log: + enabled: true diff --git a/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/logback-spring.xml b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..6bc0eafb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/application-webadmin/src/main/resources/logback-spring.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + ${LOG_PATTERN} + + + + + + + ${LOG_HOME}/${LOG_NAME}.log + true + + + ${LOG_HOME}/${LOG_NAME}-%d{yyyy-MM-dd}-%i.log + + + 31 + + + 20MB + + + + + ${LOG_PATTERN_EX} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/.DS_Store b/OrangeFormsOpen-MybatisFlex/common/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7772dea23d6f609258765ad9717841460fc20d42 GIT binary patch literal 10244 zcmeHMyKWOf6ulE75r`9BDJUqmg9eEpgeXnq`~cwtf*&yvUT<&`B|;QiQc@$tHy|Mz zDkLN*`2moqsQCoWof!r0?%uH}un@W%&0c5s+?jLEjAwVoMC3LO`|Cs*5iOx`Jm12p z;o$pRYR5vnatF~NpQxZQU8f%6q=vQ&=72e14wwVxfI09lIDlt1A4|(2+Swd12h4#J z2Y7zSP&l?@EJO5D2TpzofVhau{=jFf1AI;vV>`w&M4yT}!|K7sR2P>RE~Mjr#&E=T zjAe)+om@yK7h865h2nhc$jdyOTsuTNn*-)R-2q;^FZ&gHPPtgW4@SfFtzMDu`J~gI z-s#A^=ybZHq6-ARhUL@CS3cfNq5FS;-h22Pi`F+(OBX79`}BYgP#wBWmNgZZAV_&uQG3R}AWM#Cvw8|zecp7`ChE}8u?;}koO5k7!_lZvy8j%5%4W(H zI^Ap5S?xdWm7(Yrdj4I*DLNYKRCqUHCC`j`wkrLR`ySY(LUO(REQMA0s&y-zRlML$ zCG)P%KHBIx^Wz=~zvlY{Pbi+d_NDJHKcuh<_o|O4bt}wOyx>hg znD=UByzKkuY(bB}e=NCgu1&)${F7}c8j?9uN69+Q)pQK#F`lSUfje>rqD%bdG9^RN zGBvC5**Yal>hUjQb^S-^nIZmaZ$7u)LQN8{Xql4Js3e)wpo9Cy4s*a9FbB+m`8#kb zG@0@K|EB)?|NMzri{^kiFgFL>VrQ$ff#%}gdaYsJYtK-&QTSk78KO_Y$#2Ia`t5l9 z!`t!KP#EhsS>oq*jAe*E9T=aE7 + + + com.orangeforms + common + 1.0.0 + + 4.0.0 + + common-core + 1.0.0 + common-core + jar + + + + + com.google.guava + guava + ${guava.version} + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} + + + joda-time + joda-time + ${joda-time.version} + + + org.apache.commons + commons-collections4 + ${commons-collections4.version} + + + org.apache.commons + commons-csv + ${common-csv.version} + + + cn.hutool + hutool-all + ${hutool.version} + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + cn.jimmyshi + bean-query + ${bean.query.version} + + + + org.apache.poi + poi-ooxml + ${poi-ooxml.version} + + + + mysql + mysql-connector-java + 8.0.22 + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + com.sun + jconsole + + + com.sun + tools + + + + + com.mybatis-flex + mybatis-flex-spring-boot-starter + ${mybatisflex.version} + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java new file mode 100644 index 00000000..8d781115 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java @@ -0,0 +1,31 @@ +package com.orangeforms.common.core.advice; + +import com.orangeforms.common.core.util.MyDateUtil; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Controller的环绕拦截类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@ControllerAdvice +public class MyControllerAdvice { + + /** + * 转换前端传入的日期变量参数为指定格式。 + * + * @param binder 数据绑定参数。 + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.registerCustomEditor(Date.class, + new CustomDateEditor(new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT), false)); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java new file mode 100644 index 00000000..c39771f7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java @@ -0,0 +1,167 @@ +package com.orangeforms.common.core.advice; + +import com.orangeforms.common.core.exception.*; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.util.ContextUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.exceptions.PersistenceException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeoutException; + +/** + * 业务层的异常处理类,这里只是给出最通用的Exception的捕捉,今后可以根据业务需要, + * 用不同的函数,处理不同类型的异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@RestControllerAdvice("com.orangeforms") +public class MyExceptionHandler { + + /** + * 通用异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = Exception.class) + public ResponseResult exceptionHandle(Exception ex, HttpServletRequest request) { + log.error("Unhandled exception from URL [" + request.getRequestURI() + "]", ex); + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION, ex.getMessage()); + } + + /** + * 无效的实体对象异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidDataModelException.class) + public ResponseResult invalidDataModelExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidDataModelException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_MODEL); + } + + /** + * 无效的实体对象字段异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidDataFieldException.class) + public ResponseResult invalidDataFieldExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidDataFieldException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD); + } + + /** + * 无效类字段异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidClassFieldException.class) + public ResponseResult invalidClassFieldExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidClassFieldException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_CLASS_FIELD); + } + + /** + * 重复键异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = DuplicateKeyException.class) + public ResponseResult duplicateKeyExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("DuplicateKeyException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY); + } + + /** + * 数据访问失败异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = DataAccessException.class) + public ResponseResult dataAccessExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("DataAccessException exception from URL [" + request.getRequestURI() + "]", ex); + if (ex.getCause() instanceof PersistenceException + && ex.getCause().getCause() instanceof PermissionDeniedDataAccessException) { + return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED); + } + return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED); + } + + /** + * 操作不存在或已逻辑删除数据的异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = NoDataAffectException.class) + public ResponseResult noDataEffectExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("NoDataAffectException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + + /** + * 数据权限异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = NoDataPermException.class) + public ResponseResult noDataPermExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("NoDataPermException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED, ex.getMessage()); + } + + /** + * 自定义运行时异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = MyRuntimeException.class) + public ResponseResult myRuntimeExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("MyRuntimeException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, ex.getMessage()); + } + + /** + * Redis缓存访问异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = RedisCacheAccessException.class) + public ResponseResult redisCacheAccessExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("RedisCacheAccessException exception from URL [" + request.getRequestURI() + "]", ex); + if (ex.getCause() instanceof TimeoutException) { + return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_TIMEOUT); + } + return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_STATE_ERROR); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java new file mode 100644 index 00000000..595e6463 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记数据权限中基于DeptId进行过滤的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DeptFilterColumn { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java new file mode 100644 index 00000000..a2f5f028 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java @@ -0,0 +1,17 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 作为DisableDataFilterAspect的切点。 + * 该注解标记的方法内所有的查询语句,均不会被Mybatis拦截器过滤数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisableDataFilter { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java new file mode 100644 index 00000000..f9a89810 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 仅用于微服务的多租户项目。 + * 用于注解DAO层Mapper对象的租户过滤规则。被包含的方法将不会进行租户Id的过滤。 + * 对于tk mapper和mybatis plus中的内置方法,可以直接指定方法名即可,如:selectOne。 + * 需要说明的是,在大多数场景下,只要在实体对象中指定了租户Id字段,基于该主表的绝大部分增删改操作, + * 都需要经过租户Id过滤,仅当查询非常复杂,或者主表不在SQL语句之中的时候,可以通过该注解禁用该SQL, + * 并根据需求通过手动的方式实现租户过滤。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisableTenantFilter { + + /** + * 包含的方法名称数组。该值不能为空,因为如想取消所有方法的租户过滤, + * 可以通过在实体对象中不指定租户Id字段注解的方式实现。 + * + * @return 被包括的方法名称数组。 + */ + String[] includeMethodName(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java new file mode 100644 index 00000000..cd2f6a36 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 用于注解DAO层Mapper对象的数据权限规则。 + * 由于框架使用了tk.mapper,所以并非所有的Mapper接口均在当前Mapper对象中定义,有一部分被tk.mapper封装,如selectAll等。 + * 如果需要排除tk.mapper中的方法,可以直接使用tk.mapper基类所声明的方法名称即可。 + * 另外,比较特殊的场景是,因为tk.mapper是通用框架,所以同样的selectAll方法,可以获取不同的数据集合,因此在service中如果 + * 出现两个不同的方法调用Mapper的selectAll方法,但是一个需要参与过滤,另外一个不需要参与,那么就需要修改当前类的Mapper方法, + * 将其中一个方法重新定义一个具体的接口方法,并重新设定其是否参与数据过滤。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EnableDataPerm { + + /** + * 排除的方法名称数组。如果为空,所有的方法均会被Mybaits拦截注入权限过滤条件。 + * + * @return 被排序的方法名称数据。 + */ + String[] excluseMethodName() default {}; + + /** + * 必须包含能看用户自己数据的数据过滤条件,如果当前用户的数据过滤中,没有DataPermRuleType.TYPE_USER_ONLY, + * 在进行数据权限过滤时,会自动包含该权限。 + * + * @return 是否必须包含DataPermRuleType.TYPE_USER_ONLY类型的数据权限。 + */ + boolean mustIncludeUserRule() default false; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java new file mode 100644 index 00000000..6132c47a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 业务表中记录流程最后审批状态标记的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlowLatestApprovalStatusColumn { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java new file mode 100644 index 00000000..670a9083 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 业务表中记录流程实例结束标记的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlowStatusColumn { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java new file mode 100644 index 00000000..5546fa00 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记Job实体对象的更新时间字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JobUpdateTimeColumn { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java new file mode 100644 index 00000000..301d5427 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java @@ -0,0 +1,50 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.constant.MaskFieldTypeEnum; +import com.orangeforms.common.core.util.MaskFieldHandler; + +import java.lang.annotation.*; + +/** + * 脱敏字段注解。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MaskField { + + /** + * 脱敏类型。 + * + * @return 脱敏类型。 + */ + MaskFieldTypeEnum maskType(); + /** + * 掩码符号。 + * + * @return 掩码符号。 + */ + char maskChar() default '*'; + /** + * 前面noMaskPrefix数量的字符不被掩码。 + * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。 + * + * @return 从1开始计算,前面不被掩码的字符数。 + */ + int noMaskPrefix() default 1; + /** + * 末尾noMaskSuffix数量的字符不被掩码。 + * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。 + * + * @return 从1开始计算,末尾不被掩码的字符数。 + */ + int noMaskSuffix() default 1; + /** + * 自定义脱敏处理器接口的Class。 + * @return 自定义脱敏处理器接口的Class。 + */ + Class handler() default MaskFieldHandler.class; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java new file mode 100644 index 00000000..f12218e7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 该注解通常标记于Service中的事务方法,并且会和@Transactional注解同时存在。 + * 被注解标注的方法内代码,通常通过mybatis,并在同一个事务内访问数据库。与此同时还会存在基于 + * JDBC的跨库操作。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MultiDatabaseWriteMethod { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java new file mode 100644 index 00000000..6d516240 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记Service所依赖的数据源类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MyDataSource { + + /** + * 标注的数据源类型 + * @return 当前标注的数据源类型。 + */ + int value(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java new file mode 100644 index 00000000..41b80f8a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.util.DataSourceResolver; + +import java.lang.annotation.*; + +/** + * 基于自定义解析规则的多数据源注解。主要用于标注Service的实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MyDataSourceResolver { + + /** + * 多数据源路由键解析接口的Class。 + * @return 多数据源路由键解析接口的Class。 + */ + Class resolver(); + + /** + * DataSourceResolver.resovle方法的入参。 + * @return DataSourceResolver.resovle方法的入参。 + */ + String arg() default ""; + + /** + * 数值型参数。 + * @return DataSourceResolver.resovle方法的入参。 + */ + int intArg() default -1; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java new file mode 100644 index 00000000..4aa12b98 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标记Controller中的方法参数,参数解析器会根据该注解将请求中的JSON数据,映射到参数中的绑定字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyRequestBody { + + /** + * 是否必须出现的参数。 + */ + boolean required() default false; + /** + * 解析时用到的JSON的key。 + */ + String value() default ""; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java new file mode 100644 index 00000000..1c832ac2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java @@ -0,0 +1,15 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记无需Token验证的接口 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NoAuthInterface { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java new file mode 100644 index 00000000..5b695fb0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标识Model和常量字典之间的关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationConstDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的常量字典的Class对象。 + * + * @return 关联的常量字典的Class对象。 + */ + Class constantDictClass(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java new file mode 100644 index 00000000..7b592496 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的字典关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联Model对象的关联Name字段名称。 + * + * @return 被关联Model对象的关联Name字段名称。 + */ + String slaveNameField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 在同一个实体对象中,如果有一对一关联和字典关联,都是基于相同的主表字段,并关联到 + * 相同关联表的同一关联字段时,可以在字典关联的注解中引用被一对一注解标准的对象属性。 + * 从而在数据整合时,当前字典的数据可以直接取自"equalOneToOneRelationField"指定 + * 的字段,从而避免一次没必要的数据库查询操作,提升了加载显示的效率。 + * + * @return 与该字典字段引用关系完全相同的一对一关联属性名称。 + */ + String equalOneToOneRelationField() default ""; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java new file mode 100644 index 00000000..65ab2a5a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 全局字典关联。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationGlobalDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 全局字典编码。 + * + * @return 全局字典编码。空表示为不使用全局字典。 + */ + String dictCode(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java new file mode 100644 index 00000000..bee48192 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标注多对多的Model关系。 + * 重要提示:由于多对多关联表数据,很多时候都不需要跟随主表数据返回,所以该注解不会在 + * 生成的时候自动添加到实体类字段上,需要的时候,用户可自行手动添加。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationManyToMany { + + /** + * 多对多中间表的Mapper对象名称。 + * 如果是空字符串,BaseService会自动拼接为 relationModelClass().getSimpleName() + "Mapper"。 + * + * @return 被关联的本地Service对象名称。 + */ + String relationMapperName() default ""; + + /** + * 多对多关联表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class relationModelClass(); + + /** + * 多对多关联表Model对象中与主表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationMasterIdField(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java new file mode 100644 index 00000000..cfa48e2f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java @@ -0,0 +1,96 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 主要用于多对多的Model关系。标注通过从表关联字段或者关联表关联字段计算主表聚合计算字段的规则。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationManyToManyAggregation { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 多对多从表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 多对多从表Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 多对多关联表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class relationModelClass(); + + /** + * 多对多关联表Model对象中与主表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationMasterIdField(); + + /** + * 多对多关联表Model对象中与从表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationSlaveIdField(); + + /** + * 聚合计算所在的Model。 + * + * @return 聚合计算所在Model的Class。 + */ + Class aggregationModelClass(); + + /** + * 聚合类型。具体数值参考AggregationType对象。 + * + * @return 聚合类型。 + */ + int aggregationType(); + + /** + * 聚合计算所在Model的字段名称。 + * + * @return 聚合计算所在Model的字段名称。 + */ + String aggregationField(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java new file mode 100644 index 00000000..5a5d6e16 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的一对多关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToMany { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java new file mode 100644 index 00000000..61befd73 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 主要用于一对多的Model关系。标注通过从表关联字段计算主表聚合计算字段的规则。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToManyAggregation { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联Model对象中参与计算的聚合类型。具体数值参考AggregationType对象。 + * + * @return 被关联Model对象中参与计算的聚合类型。 + */ + int aggregationType(); + + /** + * 被关联Model对象中参与聚合计算的字段名称。 + * + * @return 被关联Model对象中参与计算字段的名称。 + */ + String aggregationField(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java new file mode 100644 index 00000000..fd38ca49 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java @@ -0,0 +1,61 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的一对一关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToOne { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 在一对一关联时,是否加载从表的字典关联。 + * + * @return 是否加载从表的字典关联。true关联,false则只返回从表自身数据。 + */ + boolean loadSlaveDict() default true; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java new file mode 100644 index 00000000..368a9ea2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记通过租户Id进行过滤的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantFilterColumn { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java new file mode 100644 index 00000000..c01e6a16 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; + +import java.lang.annotation.*; + +/** + * 用于标记支持数据上传和下载的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UploadFlagColumn { + + /** + * 上传数据存储类型。 + * + * @return 上传数据存储类型。 + */ + UploadStoreTypeEnum storeType(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java new file mode 100644 index 00000000..af9275e2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记数据权限中基于UserId进行过滤的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UserFilterColumn { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java new file mode 100644 index 00000000..5acff1a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.core.aop; + +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.config.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +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.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 多数据源AOP切面处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DataSourceAspect { + + /** + * 所有配置MyDataSource注解的Service实现类。 + */ + @Pointcut("execution(public * com.orangeforms..service..*(..)) " + + "&& @target(com.orangeforms.common.core.annotation.MyDataSource)") + public void datasourcePointCut() { + // 空注释,避免sonar警告 + } + + @Around("datasourcePointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + Class clazz = point.getTarget().getClass(); + MyDataSource ds = clazz.getAnnotation(MyDataSource.class); + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 + Integer originalType = DataSourceContextHolder.setDataSourceType(ds.value()); + log.debug("set datasource is " + ds.value()); + try { + return point.proceed(); + } finally { + DataSourceContextHolder.unset(originalType); + log.debug("unset datasource is " + originalType); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java new file mode 100644 index 00000000..f2697a64 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java @@ -0,0 +1,73 @@ +package com.orangeforms.common.core.aop; + +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.util.DataSourceResolver; +import com.orangeforms.common.core.config.DataSourceContextHolder; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +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.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 基于自定义解析规则的多数据源AOP切面处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DataSourceResolveAspect { + + private final Map, DataSourceResolver> resolverMap = new ConcurrentHashMap<>(); + + /** + * 所有配置MyDataSourceResovler注解的Service实现类。 + */ + @Pointcut("execution(public * com.orangeforms..service..*(..)) " + + "&& @target(com.orangeforms.common.core.annotation.MyDataSourceResolver)") + public void datasourceResolverPointCut() { + // 空注释,避免sonar警告 + } + + @Around("datasourceResolverPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + Class clazz = point.getTarget().getClass(); + MyDataSourceResolver dsr = clazz.getAnnotation(MyDataSourceResolver.class); + Class resolverClass = dsr.resolver(); + DataSourceResolver resolver = + resolverMap.computeIfAbsent(resolverClass, ApplicationContextHolder::getBean); + Integer type = resolver.resolve(dsr.arg(), dsr.intArg(), this.getMethodName(point), point.getArgs()); + Integer originalType = null; + if (type != null) { + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 + originalType = DataSourceContextHolder.setDataSourceType(type); + log.debug("set datasource is " + type); + } + try { + return point.proceed(); + } finally { + if (type != null) { + DataSourceContextHolder.unset(originalType); + log.debug("unset datasource is " + originalType); + } + } + } + + private String getMethodName(JoinPoint joinPoint) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + return methodSignature.getMethod().getName(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java new file mode 100644 index 00000000..940e3367 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.core.base.dao; + +import com.mybatisflex.core.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +/** + * 数据访问对象的基类。 + * + * @param 主Model实体对象。 + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseDaoMapper extends BaseMapper { + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy 分组字段列表,逗号分隔。 + * @return 对象可选字段Map列表。 + */ + @Select("") + List> getGroupedListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("groupBy") String groupBy); + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和排序字符串,返回查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 选择的字段列表。 + * @param whereClause 过滤字符串。 + * @param orderBy 排序字符串。 + * @return 查询结果。 + */ + @Select("") + List> getListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("orderBy") String orderBy); + + /** + * 用指定过滤条件,计算记录数量。 + * + * @param selectTable 表名称。 + * @param whereClause 过滤字符串。 + * @return 返回过滤后的数据数量。 + */ + @Select("") + int getCountByCondition(@Param("selectTable") String selectTable, @Param("whereClause") String whereClause); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java new file mode 100644 index 00000000..0713d5e4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java @@ -0,0 +1,124 @@ +package com.orangeforms.common.core.base.mapper; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Model对象到Domain类型对象的相互转换。实现类通常声明在Model实体类中。 + * + * @param Domain域对象类型。 + * @param Model实体对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseModelMapper { + + /** + * 转换Model实体对象到Domain域对象。 + * + * @param model Model实体对象。 + * @return Domain域对象。 + */ + D fromModel(M model); + + /** + * 转换Model实体对象列表到Domain域对象列表。 + * + * @param modelList Model实体对象列表。 + * @return Domain域对象列表。 + */ + List fromModelList(List modelList); + + /** + * 转换Domain域对象到Model实体对象。 + * + * @param domain Domain域对象。 + * @return Model实体对象。 + */ + M toModel(D domain); + + /** + * 转换Domain域对象列表到Model实体对象列表。 + * + * @param domainList Domain域对象列表。 + * @return Model实体对象列表。 + */ + List toModelList(List domainList); + + /** + * 转换bean到map + * + * @param bean bean对象。 + * @param ignoreNullValue 值为null的字段是否转换到Map。 + * @param bean类型。 + * @return 转换后的map对象。 + */ + default Map beanToMap(T bean, boolean ignoreNullValue) { + return BeanUtil.beanToMap(bean, false, ignoreNullValue); + } + + /** + * 转换bean集合到map集合 + * + * @param dataList bean对象集合。 + * @param ignoreNullValue 值为null的字段是否转换到Map。 + * @param bean类型。 + * @return 转换后的map对象集合。 + */ + default List> beanToMap(List dataList, boolean ignoreNullValue) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream() + .map(o -> BeanUtil.beanToMap(o, false, ignoreNullValue)) + .collect(Collectors.toList()); + } + + /** + * 转换map到bean。 + * + * @param map map对象。 + * @param beanClazz bean的Class对象。 + * @param bean类型。 + * @return 转换后的bean对象。 + */ + default T mapToBean(Map map, Class beanClazz) { + return BeanUtil.toBeanIgnoreError(map, beanClazz); + } + + /** + * 转换map集合到bean集合。 + * + * @param mapList map对象集合。 + * @param beanClazz bean的Class对象。 + * @param bean类型。 + * @return 转换后的bean对象集合。 + */ + default List mapToBean(List> mapList, Class beanClazz) { + if (CollUtil.isEmpty(mapList)) { + return new LinkedList<>(); + } + return mapList.stream() + .map(m -> BeanUtil.toBeanIgnoreError(m, beanClazz)) + .collect(Collectors.toList()); + } + + /** + * 对于Map字段到Map字段的映射场景,MapStruct会根据方法签名自动选择该函数 + * 作为对象copy的函数。由于该函数是直接返回的,因此没有对象copy,效率更高。 + * 如果没有该函数,MapStruct会生成如下代码: + * Map map = courseDto.getTeacherIdDictMap(); + * if ( map != null ) { + * course.setTeacherIdDictMap( new HashMap( map ) ); + * } + * + * @param map map对象。 + * @return 直接返回的map。 + */ + default Map mapToMap(Map map) { + return map; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java new file mode 100644 index 00000000..3052c396 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.core.base.mapper; + +import java.util.List; + +/** + * 哑元占位对象。Model实体对象和Domain域对象相同的场景下使用。 + * 由于没有实际的数据转换,因此同时保证了代码统一和执行效率。 + * + * @param 数据类型。 + * @author Jerry + * @date 2024-07-02 + */ +public class DummyModelMapper implements BaseModelMapper { + + /** + * 不转换直接返回。 + * + * @param model Model实体对象。 + * @return Domain域对象。 + */ + @Override + public M fromModel(M model) { + return model; + } + + /** + * 不转换直接返回。 + * + * @param modelList Model实体对象列表。 + * @return Domain域对象列表。 + */ + @Override + public List fromModelList(List modelList) { + return modelList; + } + + /** + * 不转换直接返回。 + * + * @param domain Domain域对象。 + * @return Model实体对象。 + */ + @Override + public M toModel(M domain) { + return domain; + } + + /** + * 不转换直接返回。 + * + * @param domainList Domain域对象列表。 + * @return Model实体对象列表。 + */ + @Override + public List toModelList(List domainList) { + return domainList; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java new file mode 100644 index 00000000..4235189a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java @@ -0,0 +1,40 @@ +package com.orangeforms.common.core.base.model; + +import com.mybatisflex.annotation.Column; +import lombok.Data; + +import java.util.Date; + +/** + * 实体对象的公共基类,所有子类均必须包含基类定义的数据表字段和实体对象字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class BaseModel { + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java new file mode 100644 index 00000000..c5ce2703 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java @@ -0,0 +1,229 @@ +package com.orangeforms.common.core.base.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.cache.DictionaryCache; +import com.orangeforms.common.core.object.TokenData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.*; + +/** + * 带有缓存功能的字典Service基类,需要留意的是,由于缓存基于Key/Value方式存储, + * 目前仅支持基于主键字段的缓存查找,其他条件的查找仍然从数据源获取。 + * + * @param Model实体对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class BaseDictService + extends BaseService implements IBaseDictService { + + /** + * 缓存池对象。 + */ + protected DictionaryCache dictionaryCache; + + /** + * 构造函数使用缺省缓存池对象。 + */ + protected BaseDictService() { + super(); + } + + /** + * 重新加载数据库中所有当前表数据到系统内存。 + * + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + @Override + public void reloadCachedData(boolean force) { + // 在非强制刷新情况下。 + // 先行判断缓存中是否存在数据,如果有就不加载了。 + if (!force && dictionaryCache.getCount() > 0) { + return; + } + List allList = super.getAllList(); + dictionaryCache.reload(allList, force); + } + + /** + * 保存新增对象。 + * + * @param data 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public M saveNew(M data) { + // 清空全部缓存 + dictionaryCache.invalidateAll(); + if (deletedFlagFieldName != null) { + ReflectUtil.setFieldValue(data, deletedFlagFieldName, GlobalDeletedFlag.NORMAL); + } + if (tenantIdField != null) { + ReflectUtil.setFieldValue(data, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + mapper().insert(data); + return data; + } + + /** + * 更新数据对象。 + * + * @param data 更新的对象。 + * @param originalData 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(M data, M originalData) { + dictionaryCache.invalidateAll(); + if (tenantIdField != null) { + ReflectUtil.setFieldValue(data, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + return mapper().update(data) == 1; + } + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(K id) { + dictionaryCache.invalidateAll(); + return mapper().deleteById(id) == 1; + } + + /** + * 直接从缓存池中获取主键Id关联的数据。如果缓存中不存在,再从数据库中取出并回写到缓存。 + * + * @param id 主键Id。 + * @return 主键关联的数据,不存在返回null。 + */ + @SuppressWarnings("unchecked") + @Override + public M getById(Serializable id) { + M data = dictionaryCache.get((K) id); + if (data != null) { + return data; + } + if (dictionaryCache.getCount() != 0) { + return data; + } + this.reloadCachedData(true); + return dictionaryCache.get((K) id); + } + + /** + * 直接从缓存池中获取所有数据。 + * + * @return 返回所有数据。 + */ + @Override + public List getAllListFromCache() { + List resultList = dictionaryCache.getAll(); + if (CollUtil.isNotEmpty(resultList)) { + return resultList; + } + this.reloadCachedData(true); + return dictionaryCache.getAll(); + } + + /** + * 直接从缓存池中返回符合主键 in (idValues) 条件的所有数据。 + * 对于缓存中不存在的数据,从数据库中获取并回写入缓存。 + * + * @param idValues 主键值列表。 + * @return 检索后的数据列表。 + */ + @Override + public List getInList(Set idValues) { + List resultList = dictionaryCache.getInList(idValues); + // 如果从缓存中获取与请求的id完全相同就直接返回。 + if (resultList.size() == idValues.size()) { + return resultList; + } + // 如果此时缓存中存在数据,说明有部分id是不存在的。也可以直接返回了。 + if (dictionaryCache.getCount() != 0) { + return resultList; + } + // 执行到这里,说明缓存是空的,所有需要重新加载并再次从缓存中读取并返回。 + this.reloadCachedData(true); + return dictionaryCache.getInList(idValues); + } + + @Override + public List getListByParentId(K parentId) { + List resultList = dictionaryCache.getListByParentId(parentId); + // 如果包含数据就直接返回了 + if (CollUtil.isNotEmpty(resultList)) { + return resultList; + } + // 如果缓存中存在该字典数据,说明该parentId下子对象列表为空,也可以直接返回了。 + if (this.getCachedCount() != 0) { + return resultList; + } + // 执行到这里就需要重新加载全部缓存了。 + this.reloadCachedData(true); + return dictionaryCache.getListByParentId(parentId); + } + + /** + * 返回符合 inFilterField in (inFilterValues) 条件的所有数据。属性property是主键,则从缓存中读取。 + * + * @param inFilterField 参与(In-list)过滤的Java字段。 + * @param inFilterValues 参与(In-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + @SuppressWarnings("unchecked") + @Override + public List getInList(String inFilterField, Set inFilterValues) { + if (inFilterField.equals(this.idFieldName)) { + return this.getInList((Set) inFilterValues); + } + return super.getInList(inFilterField, inFilterValues); + } + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id。 + * @param inFilterValues 数据值集合。 + * @return 全部存在返回true,否则false。 + */ + @SuppressWarnings("unchecked") + @Override + public boolean existUniqueKeyList(String inFilterField, Set inFilterValues) { + if (CollUtil.isEmpty(inFilterValues)) { + return true; + } + if (inFilterField.equals(this.idFieldName)) { + List dataList = this.getInList((Set) inFilterValues); + return dataList.size() == inFilterValues.size(); + } + String columnName = this.safeMapToColumnName(inFilterField); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(columnName, inFilterValues); + return mapper().selectCountByQuery(queryWrapper) == inFilterValues.size(); + } + + /** + * 获取缓存中的数据数量。 + * + * @return 缓存中的数据总量。 + */ + @Override + public int getCachedCount() { + return dictionaryCache.getCount(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java new file mode 100644 index 00000000..83513dcc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java @@ -0,0 +1,2278 @@ +package com.orangeforms.common.core.base.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ReflectUtil; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.constant.AggregationType; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.constant.MaskFieldTypeEnum; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import static java.util.stream.Collectors.*; + +/** + * 所有Service的基类。 + * + * @param Model对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class BaseService extends ServiceImpl, M> implements IBaseService { + + /** + * 当前Service关联的主Model实体对象的Class。 + */ + protected final Class modelClass; + /** + * 当前Service关联的主Model实体对象主键字段的Class。 + */ + protected final Class idFieldClass; + /** + * 当前Service关联的主Model实体对象的实际表名称。 + */ + protected final String tableName; + /** + * 当前Service关联的主Model对象主键字段名称。 + */ + protected String idFieldName; + /** + * 当前Service关联的主数据表中主键列名称。 + */ + protected String idColumnName; + /** + * 当前Service关联的主Model对象逻辑删除字段名称。 + */ + protected String deletedFlagFieldName; + /** + * 当前Service关联的主数据表中逻辑删除字段名称。 + */ + protected String deletedFlagColumnName; + /** + * 当前Service关联的主Model对象租户Id字段。 + */ + protected Field tenantIdField; + /** + * 流程实例状态字段。 + */ + protected Field flowStatusField; + /** + * 流程最后审批状态字段 + */ + protected Field flowLatestApprovalStatusField; + /** + * 脱敏字段列表。 + */ + protected List maskFieldList; + /** + * 当前Service关联的主Model对象租户Id字段名称。 + */ + protected String tenantIdFieldName; + /** + * 当前Service关联的主数据表中租户Id列名称。 + */ + protected String tenantIdColumnName; + /** + * 当前Job服务源主表Model对象最后更新时间字段名称。 + */ + protected String jobUpdateTimeFieldName; + /** + * 当前Job服务源主表Model对象最后更新时间列名称。 + */ + protected String jobUpdateTimeColumnName; + /** + * 当前业务服务源主表Model对象最后更新时间字段名称。 + */ + protected String updateTimeFieldName; + /** + * 当前业务服务源主表Model对象最后更新时间列名称。 + */ + protected String updateTimeColumnName; + /** + * 当前业务服务源主表Model对象最后更新用户Id字段名称。 + */ + protected String updateUserIdFieldName; + /** + * 当前业务服务源主表Model对象最后更新用户Id列名称。 + */ + protected String updateUserIdColumnName; + /** + * 当前Service关联的主Model对象主键字段赋值方法的反射对象。 + */ + protected Method setIdFieldMethod; + /** + * 当前Service关联的主Model对象主键字段访问方法的反射对象。 + */ + protected Method getIdFieldMethod; + /** + * 当前Service关联的主Model对象逻辑删除字段赋值方法的反射对象。 + */ + protected Method setDeletedFlagMethod; + /** + * 当前Service关联的全局字典对象的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List relationGlobalDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有常量字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List relationConstDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有一对一关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToOneStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有一对多关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToManyStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有多对多关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationManyToManyStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有一对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToManyAggrStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有多对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationManyToManyAggrStructList = new LinkedList<>(); + /** + * 基础表的实体对象及表信息。 + */ + protected final TableModelInfo tableModelInfo = new TableModelInfo(); + private final Map, MaskFieldHandler> maskFieldHandlerMap = new ConcurrentHashMap<>(); + + private static final String GROUPED_KEY = "GROUPED_KEY"; + private static final String AGGREGATED_VALUE = "AGGREGATED_VALUE"; + private static final String AND_OP = " AND "; + + @Override + public BaseDaoMapper getMapper() { + return mapper(); + } + + /** + * 构造函数,在实例化的时候,一次性完成所有有关主Model对象信息的加载。 + */ + @SuppressWarnings("unchecked") + protected BaseService() { + Class type = getClass(); + while (!(type.getGenericSuperclass() instanceof ParameterizedType)) { + type = type.getSuperclass(); + } + modelClass = (Class) ((ParameterizedType) type.getGenericSuperclass()).getActualTypeArguments()[0]; + idFieldClass = (Class) ((ParameterizedType) type.getGenericSuperclass()).getActualTypeArguments()[1]; + this.tableName = modelClass.getAnnotation(Table.class).value(); + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field field : fields) { + initializeField(field); + } + tableModelInfo.setModelName(modelClass.getSimpleName()); + tableModelInfo.setTableName(this.tableName); + tableModelInfo.setKeyFieldName(idFieldName); + tableModelInfo.setKeyColumnName(idColumnName); + } + + @Override + public TableModelInfo getTableModelInfo() { + return this.tableModelInfo; + } + + private void initializeField(Field field) { + if (idFieldName == null && null != field.getAnnotation(Id.class)) { + idFieldName = field.getName(); + Id c = field.getAnnotation(Id.class); + idColumnName = c == null ? idFieldName : c.value(); + setIdFieldMethod = ReflectUtil.getMethod( + modelClass, "set" + StrUtil.upperFirst(idFieldName), idFieldClass); + getIdFieldMethod = ReflectUtil.getMethod( + modelClass, "get" + StrUtil.upperFirst(idFieldName)); + } + if (null != field.getAnnotation(JobUpdateTimeColumn.class)) { + jobUpdateTimeFieldName = field.getName(); + jobUpdateTimeColumnName = this.safeMapToColumnName(jobUpdateTimeFieldName); + } + Column logicDeleteColumn = field.getAnnotation(Column.class); + if (null != logicDeleteColumn && logicDeleteColumn.isLogicDelete()) { + deletedFlagFieldName = field.getName(); + deletedFlagColumnName = this.safeMapToColumnName(deletedFlagFieldName); + setDeletedFlagMethod = ReflectUtil.getMethod( + modelClass, "set" + StrUtil.upperFirst(deletedFlagFieldName), Integer.class); + } + if (null != field.getAnnotation(TenantFilterColumn.class)) { + tenantIdField = field; + tenantIdFieldName = field.getName(); + tenantIdColumnName = this.safeMapToColumnName(tenantIdFieldName); + } + if (null != field.getAnnotation(FlowStatusColumn.class)) { + flowStatusField = field; + } + if (null != field.getAnnotation(FlowLatestApprovalStatusColumn.class)) { + flowLatestApprovalStatusField = field; + } + if (null != field.getAnnotation(MaskField.class)) { + if (maskFieldList == null) { + maskFieldList = new LinkedList<>(); + } + maskFieldList.add(field); + } + } + + /** + * 获取子类中注入的Mapper类。 + * + * @return 子类中注入的Mapper类。 + */ + protected abstract BaseDaoMapper mapper(); + + @SuppressWarnings("unchecked") + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewOrUpdate(M data, Consumer saveNew, BiConsumer update) { + if (data == null) { + return; + } + K id = (K) ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + saveNew.accept(data); + } else { + update.accept(data, this.getById(id)); + } + } + + @SuppressWarnings("unchecked") + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewOrUpdateBatch(List dataList, Consumer> saveNewBatch, BiConsumer update) { + if (CollUtil.isEmpty(dataList)) { + return; + } + List saveNewDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) == null).collect(toList()); + if (CollUtil.isNotEmpty(saveNewDataList)) { + saveNewBatch.accept(saveNewDataList); + } + List updateDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null).collect(toList()); + if (CollUtil.isNotEmpty(updateDataList)) { + for (M data : updateDataList) { + K id = (K) ReflectUtil.getFieldValue(data, idFieldName); + update.accept(data, this.getById(id)); + } + } + } + + /** + * 根据过滤条件删除数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Integer removeBy(M filter) { + return mapper().deleteByQuery(QueryWrapper.create(filter)); + } + + @Transactional(rollbackFor = Exception.class) + public boolean remove(K id) { + return mapper().deleteById(id) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateBatchOneToManyRelation( + String relationFieldName, + Object relationFieldValue, + String updateUserIdFieldName, + String updateTimeFieldName, + List dataList, + Consumer> batchInserter) { + // 删除在现有数据列表dataList中不存在的从表数据。 + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(this.safeMapToColumnName(relationFieldName), relationFieldName); + if (CollUtil.isNotEmpty(dataList)) { + Set keptIdSet = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + if (CollUtil.isNotEmpty(keptIdSet)) { + queryWrapper.notIn(idColumnName, keptIdSet); + } + } + mapper.deleteByQuery(queryWrapper); + if (CollUtil.isEmpty(dataList)) { + return; + } + // 没有包含主键的对象被视为新对象,为了效率最优化,这里执行批量插入。 + List newDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) == null).collect(toList()); + if (CollUtil.isNotEmpty(newDataList)) { + newDataList.forEach(o -> ReflectUtil.setFieldValue(o, relationFieldName, relationFieldValue)); + batchInserter.accept(newDataList); + } + // 对于主键已经存在的数据,我们视为已存在数据,这里执行逐条更新操作。 + List updateDataList = + dataList.stream().filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null).toList(); + for (M updateData : updateDataList) { + // 如果前端将更新用户Id置空,这里使用当前用户更新该字段。 + if (updateUserIdFieldName != null) { + ReflectUtil.setFieldValue(updateData, updateUserIdFieldName, TokenData.takeFromRequest().getUserId()); + } + // 如果前端将更新时间置空,这里使用当前时间更新该字段。 + if (updateTimeFieldName != null) { + ReflectUtil.setFieldValue(updateData, updateTimeFieldName, new Date()); + } + if (this.tenantIdField != null) { + ReflectUtil.setFieldValue(updateData, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + if (this.deletedFlagFieldName != null) { + ReflectUtil.setFieldValue(updateData, deletedFlagFieldName, GlobalDeletedFlag.NORMAL); + } + @SuppressWarnings("unchecked") + K id = (K) ReflectUtil.getFieldValue(updateData, idFieldName); + this.compareAndSetMaskFieldData(updateData, id); + mapper().update(updateData); + } + } + + /** + * 判断指定字段的数据是否存在,且仅仅存在一条记录。 + * 如果是基于主键的过滤,会直接调用existId过滤函数,提升性能。在有缓存的场景下,也可以利用缓存。 + * + * @param fieldName 待过滤的字段名(Java 字段)。 + * @param fieldValue 字段值。 + * @return 存在且仅存在一条返回true,否则false。 + */ + @SuppressWarnings("unchecked") + @Override + public boolean existOne(String fieldName, Object fieldValue) { + if (fieldName.equals(this.idFieldName)) { + return this.existId((K) fieldValue); + } + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + return mapper().selectCountByQuery(new QueryWrapper().eq(columnName, fieldValue)) == 1; + } + + /** + * 判断主键Id关联的数据是否存在。 + * + * @param id 主键Id。 + * @return 存在返回true,否则false。 + */ + @Override + public boolean existId(K id) { + return getById(id) != null; + } + + @Override + public M getOne(M filter) { + return mapper().selectOneByQuery(QueryWrapper.create(filter)); + } + + /** + * 返回符合 filterField = filterValue 条件的一条数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterValue 过滤的Java字段值。 + * @return 查询后的数据对象。 + */ + @SuppressWarnings("unchecked") + @Override + public M getOne(String filterField, Object filterValue) { + if (filterField.equals(idFieldName)) { + return this.getById((K) filterValue); + } + String columnName = this.safeMapToColumnName(filterField); + QueryWrapper queryWrapper = new QueryWrapper().eq(columnName, filterValue); + return mapper().selectOneByQuery(queryWrapper); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param id 主表主键Id。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 查询结果对象。 + */ + @Override + public M getByIdWithRelation(K id, MyRelationParam relationParam) { + M dataObject = this.getById(id); + this.buildRelationForData(dataObject, relationParam); + return dataObject; + } + + /** + * 获取所有数据。 + * + * @return 返回所有数据。 + */ + @Override + public List getAllList() { + return mapper().selectAll(); + } + + /** + * 获取排序后所有数据。 + * + * @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 + * @return 返回排序后所有数据。 + */ + @Override + public List getAllListByOrder(String... orderByProperties) { + String[] columns = new String[orderByProperties.length]; + for (int i = 0; i <= orderByProperties.length - 1; i++) { + columns[i] = this.safeMapToColumnName(orderByProperties[i]); + } + return mapper().selectListByQuery(new QueryWrapper().orderBy(columns)); + } + + /** + * 判断参数值主键集合中的所有数据,是否全部存在 + * + * @param idSet 待校验的主键集合。 + * @return 全部存在返回true,否则false。 + */ + @Override + public boolean existAllPrimaryKeys(Set idSet) { + if (CollUtil.isEmpty(idSet)) { + return true; + } + return this.existUniqueKeyList(idFieldName, idSet); + } + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id + * @param inFilterValues 数据值列表。 + * @return 全部存在返回true,否则false。 + */ + @Override + public boolean existUniqueKeyList(String inFilterField, Set inFilterValues) { + if (CollUtil.isEmpty(inFilterValues)) { + return true; + } + String column = this.safeMapToColumnName(inFilterField); + return mapper().selectCountByQuery(new QueryWrapper().in(column, inFilterValues)) == inFilterValues.size(); + } + + @Override + public List notExist(String filterField, Set filterSet, boolean findFirst) { + List notExistIdList = new LinkedList<>(); + int start = 0; + int count = 1000; + if (filterSet.size() > count) { + do { + int end = Math.min(filterSet.size(), start + count); + List subFilterList = CollUtil.sub(filterSet, start, end); + doNotExistQuery(filterField, subFilterList, findFirst, notExistIdList); + if ((findFirst && CollUtil.isNotEmpty(notExistIdList)) || end == filterSet.size()) { + break; + } + start += count; + } while (true); + } else { + doNotExistQuery(filterField, filterSet, findFirst, notExistIdList); + } + return notExistIdList; + } + + private void doNotExistQuery( + String filterField, Collection filterSet, boolean findFirst, List notExistIdList) { + String columnName = this.safeMapToColumnName(filterField); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(columnName, filterSet); + queryWrapper.select(columnName); + Set existIdSet = mapper().selectListByQuery(queryWrapper).stream() + .map(c -> ReflectUtil.getFieldValue(c, filterField)).collect(toSet()); + for (R filterData : filterSet) { + if (!existIdSet.contains(filterData)) { + notExistIdList.add(filterData); + if (findFirst) { + break; + } + } + } + } + + @Override + public List getInList(Set idValues) { + return this.getInList(idFieldName, idValues, null); + } + + @Override + public List getInList(String inFilterField, Set inFilterValues) { + return this.getInList(inFilterField, inFilterValues, null); + } + + @Override + public List getInList(String inFilterField, Set inFilterValues, String orderBy) { + if (CollUtil.isEmpty(inFilterValues)) { + return new LinkedList<>(); + } + String column = this.safeMapToColumnName(inFilterField); + QueryWrapper queryWrapper = new QueryWrapper().in(column, inFilterValues); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return mapper().selectListByQuery(queryWrapper); + } + + @Override + public List getInListWithRelation(Set idValues, MyRelationParam relationParam) { + List resultList = this.getInList(idValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam) { + List resultList = this.getInList(inFilterField, inFilterValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam) { + List resultList = this.getInList(inFilterField, inFilterValues, orderBy); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInList(Set idValues) { + return this.getNotInList(idFieldName, idValues, null); + } + + @Override + public List getNotInList(String inFilterField, Set inFilterValues) { + return this.getNotInList(inFilterField, inFilterValues, null); + } + + @Override + public List getNotInList(String inFilterField, Set inFilterValues, String orderBy) { + QueryWrapper queryWrapper; + if (CollUtil.isEmpty(inFilterValues)) { + queryWrapper = new QueryWrapper(); + } else { + String column = this.safeMapToColumnName(inFilterField); + queryWrapper = new QueryWrapper().notIn(column, inFilterValues); + } + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return mapper().selectListByQuery(queryWrapper); + } + + @Override + public List getNotInListWithRelation(Set idValues, MyRelationParam relationParam) { + List resultList = this.getNotInList(idValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInListWithRelation( + String inFilterField, Set inFilterValues, MyRelationParam relationParam) { + List resultList = this.getNotInList(inFilterField, inFilterValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam) { + List resultList = this.getNotInList(inFilterField, inFilterValues, orderBy); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public long getCountByFilter(M filter) { + return mapper().selectCountByQuery(QueryWrapper.create(filter)); + } + + @Override + public boolean existByFilter(M filter) { + return this.getCountByFilter(filter) > 0; + } + + @Override + public List getListByFilter(M filter) { + return mapper().selectListByQuery(QueryWrapper.create(filter)); + } + + @Override + public List getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam) { + QueryWrapper queryWrapper = filter == null ? QueryWrapper.create() : QueryWrapper.create(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + List resultList = mapper().selectListByQuery(queryWrapper); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + /** + * 获取父主键Id下的所有子数据列表。 + * + * @param parentIdFieldName 父主键字段名字,如"courseId"。 + * @param parentId 父主键的值。 + * @return 父主键Id下的所有子数据列表。 + */ + @Override + public List getListByParentId(String parentIdFieldName, K parentId) { + QueryWrapper queryWrapper = new QueryWrapper(); + String parentIdColumn = this.safeMapToColumnName(parentIdFieldName); + if (parentId != null) { + queryWrapper.eq(parentIdColumn, parentId); + } else { + queryWrapper.isNull(parentIdColumn); + } + return mapper().selectListByQuery(queryWrapper); + } + + /** + * 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectFields 选择的字段列表,多个字段逗号分隔。 + * NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 + * 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy SQL常量形式分组字段列表,逗号分隔。 + * @return 聚合计算后的数据结果集。 + */ + @Override + public List> getGroupedListByCondition( + String selectFields, String whereClause, String groupBy) { + return mapper().getGroupedListByCondition(tableName, selectFields, whereClause, groupBy); + } + + /** + * 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 + * @param filter 过滤对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param orderBy SQL常量形式排序字段列表,逗号分隔。 + * @return 查询结果。 + */ + @Override + public List getListByCondition(List selectList, M filter, String whereClause, String orderBy) { + QueryWrapper queryWrapper = filter == null ? QueryWrapper.create() : QueryWrapper.create(filter); + if (CollUtil.isNotEmpty(selectList)) { + String[] columns = new String[selectList.size()]; + for (int i = 0; i < selectList.size(); i++) { + columns[i] = this.safeMapToColumnName(selectList.get(i)); + } + queryWrapper.select(columns); + } + if (StrUtil.isNotBlank(whereClause)) { + queryWrapper.and(whereClause); + } + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return mapper().selectListByQuery(queryWrapper); + } + + /** + * 用指定过滤条件,计算记录数量。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param whereClause SQL常量形式的条件从句。 + * @return 返回过滤后的数据数量。 + */ + @Override + public Integer getCountByCondition(String whereClause) { + return mapper().getCountByCondition(this.tableName, whereClause); + } + + @Override + public void maskFieldData(M data, Set ignoreFieldSet) { + if (data != null) { + this.maskFieldDataList(CollUtil.newArrayList(data), ignoreFieldSet); + } + } + + @Override + public void maskFieldDataList(List dataList, Set ignoreFieldSet) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field maskField : maskFieldList) { + if (!CollUtil.contains(ignoreFieldSet, maskField.getName())) { + MaskField anno = maskField.getAnnotation(MaskField.class); + for (M data : dataList) { + Object maskedValue = this.doMaskFieldData(data, maskField, anno); + ReflectUtil.setFieldValue(data, maskField, maskedValue); + } + } + } + } + + @Override + public void compareAndSetMaskFieldData(M data, M originalData) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field maskField : maskFieldList) { + Object value = ReflectUtil.getFieldValue(data, maskField); + if (value == null) { + continue; + } + MaskField anno = maskField.getAnnotation(MaskField.class); + String maskChar = String.valueOf(anno.maskChar()); + // 如果此时包含了掩码字符,说明数据没有变化,就要和原字段值脱敏后的结果比对。 + // 如果一致就用脱敏前的原值,覆盖当前提交的(包含掩码的)值,否则说明进行了部分 + // 修改,但是字段值中仍然含有掩码字符,这是不允许的。 + if (value.toString().contains(maskChar)) { + Object maskedOriginalValue = this.doMaskFieldData(originalData, maskField, anno); + if (ObjectUtil.notEqual(value, maskedOriginalValue)) { + throw new MyRuntimeException("数据验证失败,不能仅修改部分脱敏数据!"); + } + Object originalValue = ReflectUtil.getFieldValue(originalData, maskField); + ReflectUtil.setFieldValue(data, maskField, originalValue); + } + } + } + + @Override + public void verifyMaskFieldData(M data) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field field : maskFieldList) { + Object value = ReflectUtil.getFieldValue(data, field); + if (value != null) { + String maskChar = String.valueOf(field.getAnnotation(MaskField.class).maskChar()); + if (value.toString().contains(maskChar)) { + throw new MyRuntimeException("数据验证失败,字段 [" + field.getName() + "] 数据存在脱敏掩码字符!"); + } + } + } + } + + @Override + public CallResult verifyRelatedData(M data, M originalData) { + return CallResult.ok(); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRelatedData(M data) { + if (data == null) { + return CallResult.ok(); + } + Object id = ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + return this.verifyRelatedData(data, null); + } + M originalData = this.getById((K) id); + if (originalData == null) { + return CallResult.error("数据验证失败,源数据不存在!"); + } + return this.verifyRelatedData(data, originalData); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRelatedData(List dataList) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 1. 先过滤出数据列表中的主键Id集合。 + Set idList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> (K) ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + // 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。 + if (CollUtil.isNotEmpty(idList)) { + // 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。 + List originalList = this.getInList(idList); + Map originalMap = originalList.stream() + .collect(toMap(c -> ReflectUtil.getFieldValue(c, idFieldName), c2 -> c2)); + // 迭代列表,传入当前最新数据和更新前数据进行比对,如果关联数据变化了,就对新数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRelatedData( + data, originalMap.get(ReflectUtil.getFieldValue(data, idFieldName))); + if (!result.isSuccess()) { + return result; + } + } + } else { + // 4. 迭代列表,传入当前最新数据,对关联数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRelatedData(data, null); + if (!result.isSuccess()) { + return result; + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForConstDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + String errorMessage = StrFormatter.format("FieldName [{}] doesn't exist", fieldName); + throw new MyRuntimeException(errorMessage); + } + RelationConstDict relationConstDict = field.getAnnotation(RelationConstDict.class); + if (relationConstDict == null) { + String errorMessage = StrFormatter.format("FieldName [{}] doesn't have RelationConstDict.", fieldName); + throw new MyRuntimeException(errorMessage); + } + Method m = ReflectUtil.getMethodByName(relationConstDict.constantDictClass(), "isValid"); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null) { + boolean ok = ReflectUtil.invokeStatic(m, id); + if (!ok) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的常量字典值 [%s]!", + relationConstDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForGlobalDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] does not exist.", fieldName)); + } + RelationGlobalDict relationGlobalDict = field.getAnnotation(RelationGlobalDict.class); + if (relationGlobalDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationGlobalDict.", fieldName)); + } + RelationStruct relationStruct = this.relationGlobalDictStructList.stream() + .filter(c -> c.relationField.getName().equals(fieldName)).findFirst().orElse(null); + Assert.notNull(relationStruct, "GlobalDictRelationStruct for [" + fieldName + "] can't be NULL"); + Map dictMap = ReflectUtil.invoke( + relationStruct.service, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), null); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictMap.containsKey(id.toString())) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的全局编码字典值 [%s]!", + relationGlobalDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] does not exist.", fieldName)); + } + RelationDict relationDict = field.getAnnotation(RelationDict.class); + if (relationDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationDict.", fieldName)); + } + BaseService service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + Set dictIdSet = service.getAllList().stream() + .map(c -> ReflectUtil.getFieldValue(c, relationDict.slaveIdField())).collect(toSet()); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictIdSet.contains(id)) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的字典表字典值 [%s]!", + relationDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForDatasourceDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist.", fieldName)); + } + RelationDict relationDict = field.getAnnotation(RelationDict.class); + if (relationDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationDict.", fieldName)); + } + // 验证数据源字典Id,由于被依赖的数据表,可能包含大量业务数据,因此还是分批做存在性比对更为高效。 + Set idSet = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(toSet()); + if (CollUtil.isNotEmpty(idSet)) { + if (idSet.iterator().next() instanceof String) { + idSet = idSet.stream().filter(c -> StrUtil.isNotBlank((String) c)).collect(toSet()); + } + BaseService slaveService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + List notExistIdList = slaveService.notExist(relationDict.slaveIdField(), idSet, true); + if (CollUtil.isNotEmpty(notExistIdList)) { + R notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的数据源表字典值 [%s]!", + relationDict.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForOneToOneRelation(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist", fieldName)); + } + RelationOneToOne relationOneToOne = field.getAnnotation(RelationOneToOne.class); + if (relationOneToOne == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationOneToOne.", fieldName)); + } + // 验证一对一关联Id,由于被依赖的数据表,可能包含大量业务数据,因此还是分批做存在性比对更为高效。 + Set idSet = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(toSet()); + if (CollUtil.isNotEmpty(idSet)) { + BaseService slaveService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToOne.slaveServiceName(), relationOneToOne.slaveModelClass())); + List notExistIdList = slaveService.notExist(relationOneToOne.slaveIdField(), idSet, true); + if (CollUtil.isNotEmpty(notExistIdList)) { + R notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的一对一关联值 [%s]!", + relationOneToOne.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + */ + @Override + public void buildRelationForDataList(List resultList, MyRelationParam relationParam) { + this.buildRelationForDataList(resultList, relationParam, null); + } + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + @Override + public void buildRelationForDataList( + List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (relationParam == null || CollUtil.isEmpty(resultList)) { + return; + } + boolean dataFilterValue = GlobalThreadLocal.setDataFilter(false); + try { + // 集成本地一对一和字段级别的数据关联。 + boolean buildOneToOne = relationParam.isBuildOneToOne() || relationParam.isBuildOneToOneWithDict(); + // 这里集成一对一关联。 + if (buildOneToOne) { + this.buildOneToOneForDataList(resultList, relationParam, ignoreFields); + } + // 集成一对多关联 + if (relationParam.isBuildOneToMany()) { + this.buildOneToManyForDataList(resultList, relationParam, ignoreFields); + } + // 这里集成多对多关联。 + if (relationParam.isBuildRelationManyToMany()) { + this.buildManyToManyForDataList(resultList, ignoreFields); + } + // 这里集成字典关联 + if (relationParam.isBuildDict()) { + // 构建全局字典关联关系 + this.buildGlobalDictForDataList(resultList, ignoreFields); + // 构建常量字典关联关系 + this.buildConstDictForDataList(resultList, ignoreFields); + this.buildDictForDataList(resultList, buildOneToOne, ignoreFields); + } + // 组装本地聚合计算关联数据 + if (relationParam.isBuildRelationAggregation()) { + // 处理多对多场景下,根据主表的结果,进行从表聚合数据的计算。 + this.buildManyToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 处理多一多场景下,根据主表的结果,进行从表聚合数据的计算。 + this.buildOneToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + } finally { + GlobalThreadLocal.setDataFilter(dataFilterValue); + } + } + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + */ + @Override + public void buildRelationForDataList(List resultList, MyRelationParam relationParam, int batchSize) { + this.buildRelationForDataList(resultList, relationParam, batchSize, null); + } + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + @Override + public void buildRelationForDataList( + List resultList, MyRelationParam relationParam, int batchSize, Set ignoreFields) { + if (CollUtil.isEmpty(resultList)) { + return; + } + if (batchSize <= 0) { + this.buildRelationForDataList(resultList, relationParam); + return; + } + int totalCount = resultList.size(); + int fromIndex = 0; + int toIndex = Math.min(batchSize, totalCount); + while (toIndex > fromIndex) { + List subResultList = resultList.subList(fromIndex, toIndex); + this.buildRelationForDataList(subResultList, relationParam, ignoreFields); + fromIndex = toIndex; + toIndex = Math.min(batchSize + fromIndex, totalCount); + } + } + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param 实体对象类型。 + */ + @Override + public void buildRelationForData(T dataObject, MyRelationParam relationParam) { + this.buildRelationForData(dataObject, relationParam, null); + } + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @param 实体对象类型。 + */ + @Override + public void buildRelationForData(T dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || relationParam == null) { + return; + } + boolean dataFilterValue = GlobalThreadLocal.setDataFilter(false); + try { + // 集成本地一对一和字段级别的数据关联。 + boolean buildOneToOne = relationParam.isBuildOneToOne() || relationParam.isBuildOneToOneWithDict(); + if (buildOneToOne) { + this.buildOneToOneForData(dataObject, relationParam, ignoreFields); + } + // 集成一对多关联 + if (relationParam.isBuildOneToMany()) { + this.buildOneToManyForData(dataObject, relationParam, ignoreFields); + } + if (relationParam.isBuildDict()) { + // 构建全局字典关联关系 + this.buildGlobalDictForData(dataObject, ignoreFields); + // 构建常量字典关联关系 + this.buildConstDictForData(dataObject, ignoreFields); + // 构建本地数据字典关联关系。 + this.buildDictForData(dataObject, buildOneToOne, ignoreFields); + } + // 组装本地聚合计算关联数据 + if (relationParam.isBuildRelationAggregation()) { + // 开始处理多对多场景。 + buildManyToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 构建一对多场景 + buildOneToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + if (relationParam.isBuildRelationManyToMany()) { + this.buildRelationManyToMany(dataObject, ignoreFields); + } + } finally { + GlobalThreadLocal.setDataFilter(dataFilterValue); + } + } + + protected void buildLocalOneToOneDictOnly(T dataObject) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToOneStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationOneToOneStructList) { + BaseService relationService = relationStruct.service; + Object relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.relationField); + if (relationObject != null) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典 + proxyTarget.buildDictForData(relationObject, false, null); + // 关联全局字典 + proxyTarget.buildGlobalDictForData(relationObject, null); + // 关联常量字典 + proxyTarget.buildConstDictForData(relationObject, null); + } + } + } + + /** + * 集成主表和多对多中间表之间的关联关系。 + * + * @param dataObject 关联后的主表数据对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRelationManyToMany(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationManyToManyStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationManyToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + RelationManyToMany r = relationStruct.relationManyToMany; + String masterIdColumn = MyModelUtil.safeMapToColumnName(r.relationMasterIdField(), r.relationModelClass()); + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, idFieldName); + Map filterMap = new HashMap<>(1); + filterMap.put(masterIdColumn, masterIdValue); + List manyToManyList = relationStruct.manyToManyMapper.selectListByMap(filterMap); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, manyToManyList); + } + } + + /** + * 为实体对象参数列表数据集成本地静态字典关联数据。 + * + * @param resultList 主表数据列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildConstDictForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.relationConstDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.relationConstDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + for (M dataObject : resultList) { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + String name = MapUtil.get(relationStruct.dictMap, id, String.class); + if (name != null) { + Map dictMap = new HashMap<>(2); + dictMap.put("id", id); + dictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, dictMap); + } + } + } + } + } + + /** + * 为实体对象参数列表数据集成全局字典关联数据。 + * + * @param resultList 主表数据列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildGlobalDictForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.relationGlobalDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.relationGlobalDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + Map dictMap = ReflectUtil.invoke( + relationStruct.service, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), masterIdSet); + MyModelUtil.makeGlobalDictRelation( + modelClass, resultList, dictMap, relationStruct.relationField.getName()); + } + } + } + + /** + * 为参数实体对象数据集成本地静态字典关联数据。 + * + * @param dataObject 实体对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildConstDictForData(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.relationConstDictStructList)) { + return; + } + for (RelationStruct relationStruct : this.relationConstDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + String name = MapUtil.get(relationStruct.dictMap, id, String.class); + if (name != null) { + Map dictMap = new HashMap<>(2); + dictMap.put("id", id); + dictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, dictMap); + } + } + } + } + + /** + * 为参数实体对象数据集成全局字典关联数据。 + * + * @param dataObject 实体对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildGlobalDictForData(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.relationGlobalDictStructList)) { + return; + } + for (RelationStruct relationStruct : this.relationGlobalDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + Map dictMap = ReflectUtil.invoke( + relationStruct.service, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), CollUtil.newHashSet(id)); + String name = dictMap.get(id.toString()); + if (name != null) { + Map reulstDictMap = new HashMap<>(2); + reulstDictMap.put("id", id); + reulstDictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, reulstDictMap); + } + } + } + } + + /** + * 为实体对象参数列表数据集成本地字典关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildDictForDataList(List resultList, boolean hasBuiltOneToOne, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + List relationList = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationList = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.equalOneToOneRelationField)) + .filter(Objects::nonNull) + .collect(toList()); + } else { + String slaveId = relationStruct.relationDict.slaveIdField(); + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + relationList = relationStruct.service.getInList(slaveId, masterIdSet); + } + } + MyModelUtil.makeDictRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + } + } + + /** + * 为实体对象数据集成本地数据字典关联数据。 + * + * @param dataObject 实体对象。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildDictForData(T dataObject, boolean hasBuiltOneToOne, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationDictStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object relationObject = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.equalOneToOneRelationField); + } else { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + relationObject = relationStruct.service.getOne(relationStruct.relationDict.slaveIdField(), id); + } + } + MyModelUtil.makeDictRelation( + modelClass, dataObject, relationObject, relationStruct.relationField.getName()); + } + } + + /** + * 为实体对象参数列表数据集成本地一对一关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param relationParam 关联从参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToOneForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToOneStructList) || CollUtil.isEmpty(resultList)) { + return; + } + boolean withDict = relationParam.isBuildOneToOneWithDict(); + for (RelationStruct relationStruct : this.localRelationOneToOneStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + BaseService relationService = relationStruct.service; + List relationList = + relationService.getInList(relationStruct.relationOneToOne.slaveIdField(), masterIdSet); + Set igoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + igoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + } + relationService.maskFieldDataList(relationList, igoreMaskFieldSet); + MyModelUtil.makeOneToOneRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + // 仅仅当需要加载从表字典关联时,才去加载。 + if (withDict && relationStruct.relationOneToOne.loadSlaveDict() && CollUtil.isNotEmpty(relationList)) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典。 + proxyTarget.buildDictForDataList(relationList, false, ignoreFields); + // 关联全局字典 + proxyTarget.buildGlobalDictForDataList(relationList, ignoreFields); + // 关联常量字典 + proxyTarget.buildConstDictForDataList(relationList, ignoreFields); + } + } + } + } + + /** + * 为实体对象数据集成本地一对一关联数据。 + * + * @param dataObject 实体对象。 + * @param relationParam 从表数据关联参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToOneForData(M dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToOneStructList)) { + return; + } + boolean withDict = relationParam.isBuildOneToOneWithDict(); + for (RelationStruct relationStruct : this.localRelationOneToOneStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + BaseService relationService = relationStruct.service; + Object relationObject = relationService.getOne(relationStruct.relationOneToOne.slaveIdField(), id); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + } + relationService.maskFieldData(relationObject, ignoreMaskFieldSet); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + // 仅仅当需要加载从表字典关联时,才去加载。 + if (withDict && relationStruct.relationOneToOne.loadSlaveDict() && relationObject != null) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典 + proxyTarget.buildDictForData(relationObject, false, ignoreFields); + // 关联全局字典 + proxyTarget.buildGlobalDictForData(relationObject, ignoreFields); + // 关联常量字典 + proxyTarget.buildConstDictForData(relationObject, ignoreFields); + } + } + } + } + + private void buildOneToManyForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToManyStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationOneToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + BaseService relationService = relationStruct.service; + List relationList = relationService.getInListWithRelation( + relationStruct.relationOneToMany.slaveIdField(), masterIdSet, MyRelationParam.dictOnly()); + MyModelUtil.makeOneToManyRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToMany.slaveModelClass().getSimpleName()); + } + for (M data : resultList) { + @SuppressWarnings("unchecked") + List relationDataList = + (List) ReflectUtil.getFieldValue(data, relationStruct.relationField.getName()); + relationService.maskFieldDataList(relationDataList, ignoreMaskFieldSet); + } + } + } + } + + private void buildOneToManyForData(M dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToManyStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationOneToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + BaseService relationService = relationStruct.service; + Set masterIdSet = new HashSet<>(1); + masterIdSet.add(id); + List relationObject = relationService.getInListWithRelation( + relationStruct.relationOneToMany.slaveIdField(), masterIdSet, MyRelationParam.dictOnly()); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToMany.slaveModelClass().getSimpleName()); + } + relationService.maskFieldDataList(relationObject, ignoreMaskFieldSet); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + } + } + } + + private void buildManyToManyForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationManyToManyStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationManyToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, idFieldName)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + RelationManyToMany r = relationStruct.relationManyToMany; + String masterIdColumn = MyModelUtil.safeMapToColumnName(r.relationMasterIdField(), r.relationModelClass()); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(masterIdColumn, masterIdSet); + List relationList = relationStruct.manyToManyMapper.selectListByQuery(queryWrapper); + MyModelUtil.makeManyToManyRelation( + modelClass, idFieldName, resultList, relationList, relationStruct.relationField.getName()); + } + } + } + + /** + * 根据实体对象参数列表和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationManyToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.localRelationManyToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationManyToManyAggrStructList) { + if (!CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + this.doBuildManyToManyAggregationForDataList(resultList, criteriaListMap, relationStruct); + } + } + } + + private void doBuildManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, RelationStruct relationStruct) { + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isEmpty(masterIdSet)) { + return; + } + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + // 提取关联中用到的各种字段和表数据。 + BasicAggregationRelationInfo basicRelationInfo = + this.parseBasicAggregationRelationInfo(relationStruct, criteriaListMap); + // 构建多表关联的where语句 + StringBuilder whereClause = new StringBuilder(256); + // 如果需要从表聚合计算或参与过滤,则需要把中间表和从表之间的关联条件加上。 + if (!basicRelationInfo.onlySelectRelationTable) { + whereClause.append(basicRelationInfo.relationTable) + .append(".") + .append(basicRelationInfo.relationSlaveColumn) + .append(" = ") + .append(basicRelationInfo.slaveTable) + .append(".") + .append(basicRelationInfo.slaveColumn); + } else { + whereClause.append("1 = 1"); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + inlistFilter.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + StringBuilder tableNames = new StringBuilder(64); + tableNames.append(basicRelationInfo.relationTable); + if (!basicRelationInfo.onlySelectRelationTable) { + tableNames.append(", ").append(basicRelationInfo.slaveTable); + } + List> aggregationMapList = + mapper().getGroupedListByCondition(tableNames.toString(), + basicRelationInfo.selectList, whereClause.toString(), basicRelationInfo.groupBy); + doMakeLocalAggregationData(aggregationMapList, resultList, relationStruct); + } + + /** + * 根据实体对象和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildManyToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationManyToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationManyToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationManyToManyAggrStructList) { + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue == null || CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + BasicAggregationRelationInfo basicRelationInfo = + this.parseBasicAggregationRelationInfo(relationStruct, criteriaListMap); + // 组装过滤条件 + String whereClause = this.makeManyToManyWhereClause( + relationStruct, masterIdValue, basicRelationInfo, criteriaListMap); + StringBuilder tableNames = new StringBuilder(64); + tableNames.append(basicRelationInfo.relationTable); + if (!basicRelationInfo.onlySelectRelationTable) { + tableNames.append(", ").append(basicRelationInfo.slaveTable); + } + List> aggregationMapList = + mapper().getGroupedListByCondition(tableNames.toString(), + basicRelationInfo.selectList, whereClause, basicRelationInfo.groupBy); + // 将查询后的结果回填到主表数据中。 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Object value = aggregationMapList.get(0).get(AGGREGATED_VALUE); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + + /** + * 根据实体对象参数列表和过滤条件,集成本地一对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + // 处理多一多场景下,根据主表的结果,进行从表聚合数据的计算。 + if (CollUtil.isEmpty(this.localRelationOneToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationOneToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + // 开始获取后面所需的各种关联数据。此部分今后可以移植到缓存中,无需每次计算。 + String slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + String slaveColumnName = MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTable, slaveColumnName, relation.slaveModelClass(), + slaveTable, relation.aggregationField(), relation.aggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + inlistFilter.setCriteria(relation.slaveModelClass(), + relation.slaveIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + List> aggregationMapList = + mapper().getGroupedListByCondition(slaveTable, selectList, criteriaString, groupBy); + doMakeLocalAggregationData(aggregationMapList, resultList, relationStruct); + } + } + } + + /** + * 根据实体对象和过滤条件,集成本地一对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationOneToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + String slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + String slaveColumnName = + MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTable, slaveColumnName, relation.slaveModelClass(), + slaveTable, relation.aggregationField(), relation.aggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + String whereClause = this.makeOneToManyWhereClause( + relationStruct, masterIdValue, slaveColumnName, criteriaListMap); + // 获取分组聚合计算结果 + List> aggregationMapList = + mapper().getGroupedListByCondition(slaveTable, selectList, whereClause, groupBy); + // 将计算结果回填到主表关联字段 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Object value = aggregationMapList.get(0).get(AGGREGATED_VALUE); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + } + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 + */ + @Override + public void loadRelationStruct() { + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field f : fields) { + initializeRelationDictStruct(f); + initializeRelationStruct(f); + initializeRelationAggregationStruct(f); + } + } + + /** + * 缺省实现返回null,在进行一对多和多对多聚合计算时,没有额外的自定义过滤条件。如有需要,需子类自行实现。 + * + * @return 自定义过滤条件列表。 + */ + protected Map> buildAggregationAdditionalWhereCriteria() { + return null; + } + + /** + * 判断当前对象的关联字段数据是否需要被验证,如果原有对象为null,表示新对象第一次插入,则必须验证。 + * + * @param object 新对象。 + * @param originalObject 原有对象。 + * @param fieldGetter 获取需要验证字段的函数对象。 + * @param 需要验证字段的类型。 + * @return 需要关联验证返回true,否则false。 + */ + protected boolean needToVerify(M object, M originalObject, Function fieldGetter) { + if (object == null) { + return false; + } + T data = fieldGetter.apply(object); + if (data == null) { + return false; + } + if (data instanceof String stringData) { + if (stringData.isEmpty()) { + return false; + } + } + if (originalObject == null) { + return true; + } + T originalData = fieldGetter.apply(originalObject); + return !data.equals(originalData); + } + + /** + * 因为Mybatis Plus中QueryWrapper的条件方法都要求传入数据表字段名,因此提供该函数将 + * Java实体对象的字段名转换为数据表字段名,如果不存在会抛出异常。 + * 另外在MyModelUtil.mapToColumnName有一级缓存,对于查询过的对象字段都会放到缓存中, + * 下次映射转换的时候,会直接从缓存获取。 + * + * @param fieldName Java实体对象的字段名。 + * @return 对应的数据表字段名。 + */ + protected String safeMapToColumnName(String fieldName) { + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + if (columnName == null) { + throw new InvalidDataFieldException(modelClass.getSimpleName(), fieldName); + } + return columnName; + } + + @SuppressWarnings("unchecked") + private void initializeRelationStruct(Field f) { + RelationOneToOne relationOneToOne = f.getAnnotation(RelationOneToOne.class); + if (relationOneToOne != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToOne.masterIdField()); + relationStruct.relationOneToOne = relationOneToOne; + if (!relationOneToOne.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationOneToOne.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToOne.slaveServiceName(), relationOneToOne.slaveModelClass())); + } + localRelationOneToOneStructList.add(relationStruct); + return; + } + RelationOneToMany relationOneToMany = f.getAnnotation(RelationOneToMany.class); + if (relationOneToMany != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToMany.masterIdField()); + relationStruct.relationOneToMany = relationOneToMany; + if (!relationOneToMany.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationOneToMany.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToMany.slaveServiceName(), relationOneToMany.slaveModelClass())); + } + localRelationOneToManyStructList.add(relationStruct); + return; + } + RelationManyToMany relationManyToMany = f.getAnnotation(RelationManyToMany.class); + if (relationManyToMany != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationManyToMany.relationMasterIdField()); + relationStruct.relationManyToMany = relationManyToMany; + String relationMapperName = relationManyToMany.relationMapperName(); + if (StrUtil.isBlank(relationMapperName)) { + relationMapperName = relationManyToMany.relationModelClass().getSimpleName() + "Mapper"; + } + relationStruct.manyToManyMapper = ApplicationContextHolder.getBean(StrUtil.lowerFirst(relationMapperName)); + localRelationManyToManyStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeRelationAggregationStruct(Field f) { + RelationOneToManyAggregation relationOneToManyAggregation = f.getAnnotation(RelationOneToManyAggregation.class); + if (relationOneToManyAggregation != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToManyAggregation.masterIdField()); + relationStruct.relationOneToManyAggregation = relationOneToManyAggregation; + if (!relationOneToManyAggregation.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationOneToManyAggregation.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationOneToManyAggregation.slaveServiceName(), relationOneToManyAggregation.slaveModelClass())); + } + localRelationOneToManyAggrStructList.add(relationStruct); + return; + } + RelationManyToManyAggregation relationManyToManyAggregation = f.getAnnotation(RelationManyToManyAggregation.class); + if (relationManyToManyAggregation != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationManyToManyAggregation.masterIdField()); + relationStruct.relationManyToManyAggregation = relationManyToManyAggregation; + if (!relationManyToManyAggregation.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationManyToManyAggregation.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationManyToManyAggregation.slaveServiceName(), relationManyToManyAggregation.slaveModelClass())); + } + localRelationManyToManyAggrStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeRelationDictStruct(Field f) { + RelationConstDict relationConstDict = f.getAnnotation(RelationConstDict.class); + if (relationConstDict != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationConstDict = relationConstDict; + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationConstDict.masterIdField()); + Field dictMapField = ReflectUtil.getField(relationConstDict.constantDictClass(), "DICT_MAP"); + relationStruct.dictMap = (Map) ReflectUtil.getStaticFieldValue(dictMapField); + relationConstDictStructList.add(relationStruct); + return; + } + RelationGlobalDict relationGlobalDict = f.getAnnotation(RelationGlobalDict.class); + if (relationGlobalDict != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationGlobalDict = relationGlobalDict; + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationGlobalDict.masterIdField()); + relationStruct.service = ApplicationContextHolder.getBean("globalDictService"); + relationStruct.globalDictMethd = ReflectUtil.getMethodByName( + relationStruct.service.getClass(), "getGlobalDictItemDictMapFromCache"); + relationGlobalDictStructList.add(relationStruct); + return; + } + RelationDict relationDict = f.getAnnotation(RelationDict.class); + if (relationDict != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationDict.masterIdField()); + relationStruct.relationDict = relationDict; + if (StrUtil.isNotBlank(relationDict.equalOneToOneRelationField())) { + relationStruct.equalOneToOneRelationField = + ReflectUtil.getField(modelClass, relationDict.equalOneToOneRelationField()); + } + if (!relationDict.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationDict.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + } + localRelationDictStructList.add(relationStruct); + } + } + + private BasicAggregationRelationInfo parseBasicAggregationRelationInfo( + RelationStruct relationStruct, Map> criteriaListMap) { + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + BasicAggregationRelationInfo relationInfo = new BasicAggregationRelationInfo(); + // 提取关联中用到的各种字段和表数据。 + relationInfo.slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + relationInfo.relationTable = MyModelUtil.mapToTableName(relation.relationModelClass()); + relationInfo.relationMasterColumn = + MyModelUtil.mapToColumnName(relation.relationMasterIdField(), relation.relationModelClass()); + relationInfo.relationSlaveColumn = + MyModelUtil.mapToColumnName(relation.relationSlaveIdField(), relation.relationModelClass()); + relationInfo.slaveColumn = MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + // 判断是否只需要关联中间表即可,从而提升查询统计的效率。 + // 1. 统计字段为中间表字段。2. 自定义过滤条件中没有基于从表字段的过滤条件。 + relationInfo.onlySelectRelationTable = + relation.aggregationModelClass().equals(relation.relationModelClass()); + if (relationInfo.onlySelectRelationTable && MapUtil.isNotEmpty(criteriaListMap)) { + List criteriaList = + criteriaListMap.get(relationStruct.relationField.getName()); + if (CollUtil.isNotEmpty(criteriaList)) { + for (MyWhereCriteria whereCriteria : criteriaList) { + if (whereCriteria.getModelClazz().equals(relation.slaveModelClass())) { + relationInfo.onlySelectRelationTable = false; + break; + } + } + } + } + String aggregationTable = relation.aggregationModelClass().equals(relation.relationModelClass()) + ? relationInfo.relationTable : relationInfo.slaveTable; + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + relationInfo.relationTable, relationInfo.relationMasterColumn, relation.aggregationModelClass(), + aggregationTable, relation.aggregationField(), relation.aggregationType()); + relationInfo.selectList = selectAndGroupByTuple.getFirst(); + relationInfo.groupBy = selectAndGroupByTuple.getSecond(); + return relationInfo; + } + + private String makeManyToManyWhereClause( + RelationStruct relationStruct, + Object masterIdValue, + BasicAggregationRelationInfo basicRelationInfo, + Map> criteriaListMap) { + StringBuilder whereClause = new StringBuilder(256); + whereClause.append(basicRelationInfo.relationTable) + .append(".").append(basicRelationInfo.relationMasterColumn); + if (masterIdValue instanceof Number) { + whereClause.append(" = ").append(masterIdValue); + } else { + whereClause.append(" = '").append(masterIdValue).append("'"); + } + // 如果需要从表聚合计算或参与过滤,则需要把中间表和从表之间的关联条件加上。 + if (!basicRelationInfo.onlySelectRelationTable) { + whereClause.append(AND_OP) + .append(basicRelationInfo.relationTable) + .append(".") + .append(basicRelationInfo.relationSlaveColumn) + .append(" = ") + .append(basicRelationInfo.slaveTable) + .append(".") + .append(basicRelationInfo.slaveColumn); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relationStruct.relationManyToManyAggregation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (CollUtil.isNotEmpty(criteriaList)) { + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + } + return whereClause.toString(); + } + + private String makeOneToManyWhereClause( + RelationStruct relationStruct, + Object masterIdValue, + String slaveColumnName, + Map> criteriaListMap) { + StringBuilder whereClause = new StringBuilder(64); + if (masterIdValue instanceof Number) { + whereClause.append(slaveColumnName).append(" = ").append(masterIdValue); + } else { + whereClause.append(slaveColumnName).append(" = '").append(masterIdValue).append("'"); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relationStruct.relationOneToManyAggregation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (CollUtil.isNotEmpty(criteriaList)) { + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + } + return whereClause.toString(); + } + + private static class BasicAggregationRelationInfo { + private String slaveTable; + private String slaveColumn; + private String relationTable; + private String relationMasterColumn; + private String relationSlaveColumn; + private String selectList; + private String groupBy; + private boolean onlySelectRelationTable; + } + + private void doMakeLocalAggregationData( + List> aggregationMapList, List resultList, RelationStruct relationStruct) { + if (CollUtil.isEmpty(resultList)) { + return; + } + // 根据获取的分组聚合结果集,绑定到主表总的关联字段。 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Map relatedMap = new HashMap<>(aggregationMapList.size()); + String groupedKey = GROUPED_KEY; + String aggregatedValue = AGGREGATED_VALUE; + if (!aggregationMapList.get(0).containsKey(groupedKey)) { + groupedKey = groupedKey.toLowerCase(); + aggregatedValue = aggregatedValue.toLowerCase(); + } + for (Map map : aggregationMapList) { + relatedMap.put(map.get(groupedKey).toString(), map.get(aggregatedValue)); + } + for (M dataObject : resultList) { + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + Object value = relatedMap.get(masterIdValue.toString()); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + } + + private Tuple2 makeSelectListAndGroupByClause( + String groupTableName, + String groupColumnName, + Class aggregationModel, + String aggregationTableName, + String aggregationField, + Integer aggregationType) { + if (!AggregationType.isValid(aggregationType)) { + throw new IllegalArgumentException("Invalid AggregationType Value [" + + aggregationType + "] in Model [" + aggregationModel.getName() + "]."); + } + String aggregationFunc = AggregationType.getAggregationFunction(aggregationType); + String aggregationColumn = MyModelUtil.mapToColumnName(aggregationField, aggregationModel); + if (StrUtil.isBlank(aggregationColumn)) { + throw new IllegalArgumentException("Invalid AggregationField [" + + aggregationField + "] in Model [" + aggregationModel.getName() + "]."); + } + // 构建Select List + // 如:r_table.master_id groupedKey, SUM(r_table.aggr_column) aggregated_value + StringBuilder groupedSelectList = new StringBuilder(128); + groupedSelectList.append(groupTableName) + .append(".") + .append(groupColumnName) + .append(" ") + .append(GROUPED_KEY) + .append(", ") + .append(aggregationFunc) + .append("(") + .append(aggregationTableName) + .append(".") + .append(aggregationColumn) + .append(") ") + .append(AGGREGATED_VALUE) + .append(" "); + StringBuilder groupBy = new StringBuilder(64); + groupBy.append(groupTableName).append(".").append(groupColumnName); + return new Tuple2<>(groupedSelectList.toString(), groupBy.toString()); + } + + private Object doMaskFieldData(M data, Field maskField, MaskField anno) { + Object value = ReflectUtil.getFieldValue(data, maskField); + if (value == null) { + return value; + } + if (anno.maskType().equals(MaskFieldTypeEnum.NAME)) { + value = MaskFieldUtil.chineseName(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.MOBILE_PHONE)) { + value = MaskFieldUtil.mobilePhone(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.FIXED_PHONE)) { + value = MaskFieldUtil.fixedPhone(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.EMAIL)) { + value = MaskFieldUtil.email(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.ID_CARD)) { + value = MaskFieldUtil.idCardNum(value.toString(), anno.noMaskPrefix(), anno.noMaskSuffix(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.BANK_CARD)) { + value = MaskFieldUtil.bankCard(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.CAR_LICENSE)) { + value = MaskFieldUtil.carLicense(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.CUSTOM)) { + MaskFieldHandler handler = + maskFieldHandlerMap.computeIfAbsent(anno.handler(), ApplicationContextHolder::getBean); + value = handler.handleMask(modelClass.getSimpleName(), maskField.getName(), value.toString(), anno.maskChar()); + } + return value; + } + + private void compareAndSetMaskFieldData(M data, K id) { + if (CollUtil.isNotEmpty(maskFieldList)) { + M originalData = this.getById(id); + this.compareAndSetMaskFieldData(data, originalData); + } + } + + private String getNormalizedSlaveServiceName(String slaveServiceName, Class slaveModelClass) { + if (StrUtil.isBlank(slaveServiceName)) { + slaveServiceName = slaveModelClass.getSimpleName() + "Service"; + } + return StrUtil.lowerFirst(slaveServiceName); + } + + @Data + public static class RelationStruct { + private Field relationField; + private Field masterIdField; + private Field equalOneToOneRelationField; + private Method globalDictMethd; + private BaseService service; + private BaseDaoMapper manyToManyMapper; + private Map dictMap; + private RelationConstDict relationConstDict; + private RelationGlobalDict relationGlobalDict; + private RelationDict relationDict; + private RelationOneToOne relationOneToOne; + private RelationOneToMany relationOneToMany; + private RelationManyToMany relationManyToMany; + private RelationOneToManyAggregation relationOneToManyAggregation; + private RelationManyToManyAggregation relationManyToManyAggregation; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java new file mode 100644 index 00000000..556b70b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.core.base.service; + +import java.io.Serializable; +import java.util.List; + +/** + * 带有缓存功能的字典Service接口。 + * + * @param Model实体对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface IBaseDictService extends IBaseService { + + /** + * 重新加载数据库中所有当前表数据到系统内存。 + * + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + void reloadCachedData(boolean force); + + /** + * 保存新增对象。 + * + * @param data 新增对象。 + * @return 返回新增对象。 + */ + M saveNew(M data); + + /** + * 更新数据对象。 + * + * @param data 更新的对象。 + * @param originalData 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(M data, M originalData); + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(K id); + + /** + * 直接从缓存池中获取所有数据。 + * + * @return 返回所有数据。 + */ + List getAllListFromCache(); + + /** + * 根据父主键Id,获取子对象列表。 + * + * @param parentId 上级行政区划Id。 + * @return 下级行政区划列表。 + */ + List getListByParentId(K parentId); + + /** + * 获取缓存中的数据数量。 + * + * @return 缓存中的数据总量。 + */ + int getCachedCount(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java new file mode 100644 index 00000000..37bcdf56 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java @@ -0,0 +1,559 @@ +package com.orangeforms.common.core.base.service; + +import com.mybatisflex.core.service.IService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TableModelInfo; + +import java.io.Serializable; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * 所有Service的接口。 + * + * @param Model对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface IBaseService extends IService { + + /** + * 如果主键存在则更新,否则新增保存实体对象。 + * + * @param data 实体对象数据。 + * @param saveNew 新增实体对象方法。 + * @param update 更新实体对象方法。 + */ + void saveNewOrUpdate(M data, Consumer saveNew, BiConsumer update); + + /** + * 如果主键存在的则更新,否则批量新增保存实体对象。 + * + * @param dataList 实体对象数据列表。 + * @param saveNewBatch 批量新增实体对象方法。 + * @param update 更新实体对象方法。 + */ + void saveNewOrUpdateBatch(List dataList, Consumer> saveNewBatch, BiConsumer update); + + /** + * 根据过滤条件删除数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + Integer removeBy(M filter); + + /** + * 基于主从表之间的关联字段,批量改更新一对多从表数据。 + * 该操作会覆盖增、删、改三个操作,具体如下: + * 1. 先删除。从表中relationFieldName字段的值为relationFieldValue, 同时主键Id不在dataList中的。 + * 2. 再批量插入。遍历dataList中没有主键Id的对象,视为新对象批量插入。 + * 3. 最后逐条更新,遍历dataList中有主键Id的对象,视为已存在对象并逐条更新。 + * 4. 如果更新时间和更新用户Id为空,我们将视当前记录为变化数据,因此使用当前时间和用户分别填充这两个字段。 + * + * @param relationFieldName 主从表关联中,从表的Java字段名。 + * @param relationFieldValue 主从表关联中,与从表关联的主表字段值。该值会被赋值给从表关联字段。 + * @param updateUserIdFieldName 一对多从表的更新用户Id字段名。 + * @param updateTimeFieldName 一对多从表的更新时间字段名 + * @param dataList 批量更新的从表数据列表。 + * @param batchInserter 从表批量插入方法。 + */ + void updateBatchOneToManyRelation( + String relationFieldName, + Object relationFieldValue, + String updateUserIdFieldName, + String updateTimeFieldName, + List dataList, + Consumer> batchInserter); + + /** + * 判断指定字段的数据是否存在,且仅仅存在一条记录。 + * 如果是基于主键的过滤,会直接调用existId过滤函数,提升性能。在有缓存的场景下,也可以利用缓存。 + * + * @param fieldName 待过滤的字段名(Java 字段)。 + * @param fieldValue 字段值。 + * @return 存在且仅存在一条返回true,否则false。 + */ + boolean existOne(String fieldName, Object fieldValue); + + /** + * 判断主键Id关联的数据是否存在。 + * + * @param id 主键Id。 + * @return 存在返回true,否则false。 + */ + boolean existId(K id); + + /** + * 返回符合过滤条件的一条数据。 + * + * @param filter 过滤的Java对象。 + * @return 查询后的数据对象。 + */ + M getOne(M filter); + + /** + * 返回符合 filterField = filterValue 条件的一条数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterValue 过滤的Java字段值。 + * @return 查询后的数据对象。 + */ + M getOne(String filterField, Object filterValue); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param id 主表主键Id。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 查询结果对象。 + */ + M getByIdWithRelation(K id, MyRelationParam relationParam); + + /** + * 获取所有数据。 + * + * @return 返回所有数据。 + */ + List getAllList(); + + /** + * 获取排序后所有数据。 + * + * @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 + * @return 返回排序后所有数据。 + */ + List getAllListByOrder(String... orderByProperties); + + /** + * 判断参数值主键集合中的所有数据,是否全部存在 + * + * @param idSet 待校验的主键集合。 + * @return 全部存在返回true,否则false。 + */ + boolean existAllPrimaryKeys(Set idSet); + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id + * @param inFilterValues 数据值列表。 + * @return 全部存在返回true,否则false。 + */ + boolean existUniqueKeyList(String inFilterField, Set inFilterValues); + + /** + * 根据过滤字段和过滤集合,返回不存在的数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterSet 过滤字段数据集合。 + * @param findFirst 是否找到第一个就返回。 + * @param 过滤字段类型。 + * @return filterSet中,在从表中不存在的数据集合。 + */ + List notExist(String filterField, Set filterSet, boolean findFirst); + + /** + * 返回符合主键 IN (idValues) 条件的所有数据。 + * + * @param idValues 主键值集合。 + * @return 检索后的数据列表。 + */ + List getInList(Set idValues); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + List getInList(String inFilterField, Set inFilterValues); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @return 检索后的数据列表。 + */ + List getInList(String inFilterField, Set inFilterValues, String orderBy); + + /** + * 返回符合主键 IN (idValues) 条件的所有数据。同时返回关联数据。 + * + * @param idValues 主键值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation(Set idValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。同时返回关联数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam); + + /** + * 返回符合主键 NOT IN (idValues) 条件的所有数据。 + * + * @param idValues 主键值集合。 + * @return 检索后的数据列表。 + */ + List getNotInList(Set idValues); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + List getNotInList(String inFilterField, Set inFilterValues); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @return 检索后的数据列表。 + */ + List getNotInList(String inFilterField, Set inFilterValues, String orderBy); + + /** + * 返回符合主键 NOT IN (idValues) 条件的所有数据。同时返回关联数据。 + * + * @param idValues 主键值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation(Set idValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。同时返回关联数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam); + + /** + * 用参数对象作为过滤条件,获取数据数量。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。 + * @return 返回过滤后的数据数量。 + */ + long getCountByFilter(M filter); + + /** + * 用参数对象作为过滤条件,判断是否存在过滤数据。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。 + * @return 存在返回true,否则false。 + */ + boolean existByFilter(M filter); + + /** + * 用参数对象作为过滤条件,获取查询结果。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。如果参数为null,则返回全部数据。 + * @return 返回过滤后的数据。 + */ + List getListByFilter(M filter); + + /** + * 用参数对象作为过滤条件,获取查询结果。同时查询并绑定关联数据。 + * + * @param filter 该方法基于mybatis的通用mapper。如果参数为null,则返回全部数据。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 返回过滤后的数据。 + */ + List getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam); + + /** + * 获取父主键Id下的所有子数据列表。 + * + * @param parentIdFieldName 父主键字段名字,如"courseId"。 + * @param parentId 父主键的值。 + * @return 父主键Id下的所有子数据列表。 + */ + List getListByParentId(String parentIdFieldName, K parentId); + + /** + * 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectFields 选择的字段列表,多个字段逗号分隔。 + * NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 + * 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy SQL常量形式分组字段列表,逗号分隔。 + * @return 聚合计算后的数据结果集。 + */ + List> getGroupedListByCondition(String selectFields, String whereClause, String groupBy); + + /** + * 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 + * @param filter 过滤对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param orderBy SQL常量形式排序字段列表,逗号分隔。 + * @return 查询结果。 + */ + List getListByCondition(List selectList, M filter, String whereClause, String orderBy); + + /** + * 用指定过滤条件,计算记录数量。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param whereClause SQL常量形式的条件从句。 + * @return 返回过滤后的数据数量。 + */ + Integer getCountByCondition(String whereClause); + + /** + * 仅对标记MaskField注解的字段数据进行脱敏。 + * + * @param data 实体对象。 + * @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 + */ + void maskFieldData(M data, Set ignoreFieldSet); + + /** + * 仅对标记MaskField注解的字段数据进行脱敏。 + * + * @param dataList 实体对象列表。 + * @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 + */ + void maskFieldDataList(List dataList, Set ignoreFieldSet); + + /** + * 比较并处理脱敏字段的数据变化。 + * 如果data对象中的脱敏字段值和originalData字段的脱敏后值相同,表示当前data对象的脱敏字段数据没有变化, + * 因此需要使用数据库中的原有字段值,覆盖当前实体对象中的该字段值,以保证数据库表字段中始终存储的是未脱敏数据。 + * + * @param data 当前数据对象。 + * @param originalData 原数据对象。 + */ + void compareAndSetMaskFieldData(M data, M originalData); + + /** + * 对标记MaskField注解的脱敏字段进行判断。字段数据中不能包含脱敏掩码字符。 + * + * @param data 实体对象。 + */ + void verifyMaskFieldData(M data); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中会给出返回CallResult.ok()的缺省实现。每个业务服务实现类在需要的时候可以重载该方法。 + * + * @param data 数据对象。 + * @param originalData 原有数据对象,null表示data为新增对象。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(M data, M originalData); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * 如果data对象中包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param data 数据对象。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(M data); + + /** + * 根据最新对象列表和原有对象列表的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * 如果dataList列表中的对象包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param dataList 数据对象列表。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(List dataList); + + /** + * 批量导入数据列表,对依赖全局字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖全局字典的字段名,包含RelationGlobalDict注解的字段。 + * @param idGetter 获取业务主表中依赖全局字典字段值的Function对象。 + * @param 业务主表中依全局字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForGlobalDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖常量字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖常量字典的字段名,包含RelationConstDict注解的字段。 + * @param idGetter 获取业务主表中依赖常量字典字段值的Function对象。 + * @param 业务主表中依赖常量字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForConstDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖字典表字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖字典表字典的字段名,包含RelationDict注解的字段。 + * @param idGetter 获取业务主表中依赖字典表字典字段值的Function对象。 + * @param 业务主表中依赖字典表字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖数据源字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖数据源字典的字段名,包含RelationDict注解的字段的数据源字典。 + * @param idGetter 获取业务主表中依赖数据源字典字段值的Function对象。 + * @param 业务主表中依赖数据源字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForDatasourceDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对存在一对一关联的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中存在一对一关联的字段名,包含RelationOneToOne注解的字段。 + * @param idGetter 获取业务主表中一对一关联字段值的Function对象。 + * @param 业务主表中存在一对一关联的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForOneToOneRelation(List dataList, String fieldName, Function idGetter); + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam); + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields); + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam, int batchSize); + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + void buildRelationForDataList( + List resultList, MyRelationParam relationParam, int batchSize, Set ignoreFields); + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param 实体对象类型。 + */ + void buildRelationForData(T dataObject, MyRelationParam relationParam); + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @param 实体对象类型。 + */ + void buildRelationForData(T dataObject, MyRelationParam relationParam, Set ignoreFields); + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 + */ + void loadRelationStruct(); + + /** + * 获取当前服务引用的实体对象及表信息。 + * + * @return 实体对象及表信息。 + */ + TableModelInfo getTableModelInfo(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java new file mode 100644 index 00000000..a4313a53 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.core.base.vo; + +import lombok.Data; + +import java.util.Date; + +/** + * VO对象的公共基类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class BaseVo { + + /** + * 创建者Id。 + */ + private Long createUserId; + + /** + * 创建时间。 + */ + private Date createTime; + + /** + * 更新者Id。 + */ + private Long updateUserId; + + /** + * 更新时间。 + */ + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java new file mode 100644 index 00000000..203eafd1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java @@ -0,0 +1,110 @@ +package com.orangeforms.common.core.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * 使用Caffeine作为本地缓存库 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@EnableCaching +public class CacheConfig { + + private static final int DEFAULT_MAXSIZE = 10000; + private static final int DEFAULT_TTL = 3600; + + /** + * 定义cache名称、超时时长秒、最大个数 + * 每个cache缺省3600秒过期,最大个数1000 + */ + public enum CacheEnum { + /** + * 专门存储用户权限的缓存(600秒)。 + */ + USER_PERMISSION_CACHE(600, 10000), + /** + * 专门存储用户权限字的缓存(600秒)。仅当使用satoken权限框架时可用。 + */ + USER_PERM_CODE_CACHE(600, 10000), + /** + * 专门存储用户数据权限的缓存(600秒)。 + */ + DATA_PERMISSION_CACHE(600, 10000), + /** + * 专门存储用户菜单关联权限的缓存(600秒)。 + */ + MENU_PERM_CACHE(600, 10000), + /** + * 存储指定部门Id集合的所有子部门Id集合。 + */ + CHILDREN_DEPT_ID_CACHE(1800, 10000), + /** + * 在线表单组件渲染数据缓存。 + */ + ONLINE_FORM_RENDER_CACCHE(300, 100), + /** + * 报表表单组件渲染数据缓存。 + */ + REPORT_FORM_RENDER_CACCHE(300, 100), + /** + * 缺省全局缓存(时间是24小时)。 + */ + GLOBAL_CACHE(86400, 20000); + + CacheEnum() { + } + + CacheEnum(int ttl, int maxSize) { + this.ttl = ttl; + this.maxSize = maxSize; + } + + /** + * 缓存的最大数量。 + */ + private int maxSize = DEFAULT_MAXSIZE; + /** + * 缓存的时长(单位:秒) + */ + private int ttl = DEFAULT_TTL; + + public int getMaxSize() { + return maxSize; + } + + public int getTtl() { + return ttl; + } + } + + /** + * 初始化缓存配置。这里为了有别于Redisson的缓存。 + */ + @Bean("caffeineCacheManager") + public CacheManager cacheManager() { + SimpleCacheManager manager = new SimpleCacheManager(); + // 把各个cache注册到cacheManager中,CaffeineCache实现了org.springframework.cache.Cache接口 + ArrayList caches = new ArrayList<>(); + for (CacheEnum c : CacheEnum.values()) { + caches.add(new CaffeineCache(c.name(), + Caffeine.newBuilder().recordStats() + .expireAfterWrite(c.getTtl(), TimeUnit.SECONDS) + .maximumSize(c.getMaxSize()) + .build()) + ); + } + manager.setCaches(caches); + return manager; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java new file mode 100644 index 00000000..14fe0391 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java @@ -0,0 +1,89 @@ +package com.orangeforms.common.core.cache; + +import java.util.List; +import java.util.Set; + +/** + * 主要用于完整缓存字典表数据的接口对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface DictionaryCache { + + /** + * 按照数据插入的顺序返回全部字典对象的列表。 + * + * @return 全部字段数据列表。 + */ + List getAll(); + + /** + * 获取缓存中与键列表对应的对象列表。 + * + * @param keys 主键集合。 + * @return 对象列表。 + */ + List getInList(Set keys); + + /** + * 重新加载。如果数据列表为空,则会清空原有缓存数据。 + * + * @param dataList 待缓存的数据列表。 + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + void reload(List dataList, boolean force); + + /** + * 从缓存中获取指定的数据。 + * + * @param key 数据的key。 + * @return 获取到的数据,如果没有返回null。 + */ + V get(K key); + + /** + * 将数据存入缓存。 + * + * @param key 通常为字典数据的主键。 + * @param object 字典数据对象。 + */ + void put(K key, V object); + + /** + * 获取缓存中数据条目的数量。 + * + * @return 返回缓存的数据数量。 + */ + int getCount(); + + /** + * 删除缓存中指定的键。 + * + * @param key 待删除数据的主键。 + * @return 返回被删除的对象,如果主键不存在,返回null。 + */ + V invalidate(K key); + + /** + * 删除缓存中,参数列表中包含的键。 + * + * @param keys 待删除数据的主键集合。 + */ + void invalidateSet(Set keys); + + /** + * 清空缓存。 + */ + void invalidateAll(); + + /** + * 根据父主键Id获取所有子对象的列表。 + * + * @param parentId 父主键Id。如果parentId为null,则返回所有一级节点数据。 + * @return 所有子对象的列表。 + */ + default List getListByParentId(K parentId) { throw new UnsupportedOperationException(); } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java new file mode 100644 index 00000000..7f238801 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java @@ -0,0 +1,200 @@ +package com.orangeforms.common.core.cache; + +import cn.hutool.core.map.MapUtil; +import com.orangeforms.common.core.exception.MapCacheAccessException; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 字典数据内存缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MapDictionaryCache implements DictionaryCache { + + /** + * 存储字典数据的Map。 + */ + protected final LinkedHashMap dataMap = new LinkedHashMap<>(); + /** + * 获取字典主键数据的函数对象。 + */ + protected final Function idGetter; + /** + * 由于大部分场景是读取操作,所以使用读写锁提高并发的伸缩性。 + */ + protected final ReadWriteLock lock = new ReentrantReadWriteLock(); + /** + * 超时时长。单位毫秒。 + */ + protected static final long TIMEOUT = 2000L; + + /** + * 当前对象的构造器函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的字典内存缓存对象。 + */ + public static MapDictionaryCache create(Function idGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + return new MapDictionaryCache<>(idGetter); + } + + /** + * 构造函数。 + * + * @param idGetter 主键Id的获取函数对象。 + */ + public MapDictionaryCache(Function idGetter) { + this.idGetter = idGetter; + } + + @Override + public List getAll() { + return this.safeRead("getAll", () -> { + List resultList = new LinkedList<>(); + if (MapUtil.isNotEmpty(dataMap)) { + resultList.addAll(dataMap.values()); + } + return resultList; + }); + } + + @Override + public List getInList(Set keys) { + return this.safeRead("getInList", () -> { + List resultList = new LinkedList<>(); + keys.forEach(key -> { + V object = dataMap.get(key); + if (object != null) { + resultList.add(object); + } + }); + return resultList; + }); + } + + @Override + public V get(K id) { + if (id == null) { + return null; + } + return this.safeRead("get", () -> dataMap.get(id)); + } + + @Override + public void reload(List dataList, boolean force) { + if (!force && this.getCount() > 0) { + return; + } + this.safeWrite("reload", () -> { + dataMap.clear(); + dataList.forEach(dataObj -> { + K id = idGetter.apply(dataObj); + dataMap.put(id, dataObj); + }); + return null; + }); + } + + @Override + public void put(K id, V object) { + this.safeWrite("put", () -> dataMap.put(id, object)); + } + + @Override + public int getCount() { + return dataMap.size(); + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + return this.safeWrite("invalidate", () -> dataMap.remove(id)); + } + + @Override + public void invalidateSet(Set keys) { + this.safeWrite("invalidateSet", () -> { + keys.forEach(id -> { + if (id != null) { + dataMap.remove(id); + } + }); + return null; + }); + } + + @Override + public void invalidateAll() { + this.safeWrite("invalidateAll", () -> { + dataMap.clear(); + return null; + }); + } + + protected T safeRead(String functionName, Supplier supplier) { + String exceptionMessage; + try { + if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { + try { + return supplier.get(); + } finally { + lock.readLock().unlock(); + } + } else { + throw new TimeoutException(); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + exceptionMessage = String.format( + "LOCK Operation of [MapDictionaryCache::%s] encountered EXCEPTION [%s] for DICT.", + functionName, e.getClass().getSimpleName()); + log.warn(exceptionMessage); + throw new MapCacheAccessException(exceptionMessage, e); + } + } + + protected T safeWrite(String functionName, Supplier supplier) { + String exceptionMessage; + try { + if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { + try { + return supplier.get(); + } finally { + lock.writeLock().unlock(); + } + } else { + throw new TimeoutException(); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + exceptionMessage = String.format( + "LOCK Operation of [MapDictionaryCache::%s] encountered EXCEPTION [%s] for DICT.", + functionName, e.getClass().getSimpleName()); + log.warn(exceptionMessage); + throw new MapCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java new file mode 100644 index 00000000..b492ebe2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java @@ -0,0 +1,138 @@ +package com.orangeforms.common.core.cache; + +import cn.hutool.core.collection.CollUtil; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; + +/** + * 树形字典数据内存缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MapTreeDictionaryCache extends MapDictionaryCache { + + /** + * 树形数据存储对象。 + */ + private final Multimap allTreeMap = LinkedHashMultimap.create(); + /** + * 获取字典父主键数据的函数对象。 + */ + protected final Function parentIdGetter; + + /** + * 当前对象的构造器函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的树形字典内存缓存对象。 + */ + public static MapTreeDictionaryCache create(Function idGetter, Function parentIdGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + if (parentIdGetter == null) { + throw new IllegalArgumentException("ParentIdGetter can't be NULL."); + } + return new MapTreeDictionaryCache<>(idGetter, parentIdGetter); + } + + /** + * 构造函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + */ + public MapTreeDictionaryCache(Function idGetter, Function parentIdGetter) { + super(idGetter); + this.parentIdGetter = parentIdGetter; + } + + @Override + public void reload(List dataList, boolean force) { + if (!force && this.getCount() > 0) { + return; + } + this.safeWrite("reload", () -> { + dataMap.clear(); + allTreeMap.clear(); + dataList.forEach(data -> { + K id = idGetter.apply(data); + dataMap.put(id, data); + K parentId = parentIdGetter.apply(data); + allTreeMap.put(parentId, data); + }); + return null; + }); + } + + @Override + public List getListByParentId(K parentId) { + return this.safeRead("getListByParentId", () -> { + List resultList = new LinkedList<>(); + Collection children = allTreeMap.get(parentId); + if (CollUtil.isNotEmpty(children)) { + resultList.addAll(children); + } + return resultList; + }); + } + + @Override + public void put(K id, V data) { + this.safeWrite("put", () -> { + dataMap.put(id, data); + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, data); + allTreeMap.put(parentId, data); + return null; + }); + } + + @Override + public V invalidate(K id) { + return this.safeWrite("invalidate", () -> { + V v = dataMap.remove(id); + if (v != null) { + K parentId = parentIdGetter.apply(v); + allTreeMap.remove(parentId, v); + } + return v; + }); + } + + @Override + public void invalidateSet(Set keys) { + this.safeWrite("invalidateSet", () -> { + keys.forEach(id -> { + if (id != null) { + V data = dataMap.remove(id); + if (data != null) { + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, data); + } + } + }); + return null; + }); + } + + @Override + public void invalidateAll() { + this.safeWrite("invalidateAll", () -> { + dataMap.clear(); + allTreeMap.clear(); + return null; + }); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java new file mode 100644 index 00000000..369fcf33 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java @@ -0,0 +1,60 @@ +package com.orangeforms.common.core.config; + +import com.alibaba.druid.pool.DruidDataSource; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 基于Druid的数据源配置的基类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "spring.datasource.druid") +public class BaseMultiDataSourceConfig { + + private String driverClassName; + private String name; + private Integer initialSize; + private Integer minIdle; + private Integer maxActive; + private Integer maxWait; + private Integer timeBetweenEvictionRunsMillis; + private Integer minEvictableIdleTimeMillis; + private Boolean poolPreparedStatements; + private Integer maxPoolPreparedStatementPerConnectionSize; + private Integer maxOpenPreparedStatements; + private String validationQuery; + private Boolean testWhileIdle; + private Boolean testOnBorrow; + private Boolean testOnReturn; + + /** + * 将连接池的通用配置应用到数据源对象上。 + * + * @param druidDataSource Druid的数据源。 + * @return 应用后的Druid数据源。 + */ + protected DruidDataSource applyCommonProps(DruidDataSource druidDataSource) { + druidDataSource.setConnectionErrorRetryAttempts(5); + druidDataSource.setDriverClassName(driverClassName); + druidDataSource.setName(name); + druidDataSource.setInitialSize(initialSize); + druidDataSource.setMinIdle(minIdle); + druidDataSource.setMaxActive(maxActive); + druidDataSource.setMaxWait(maxWait); + druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + druidDataSource.setPoolPreparedStatements(poolPreparedStatements); + druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); + druidDataSource.setMaxOpenPreparedStatements(maxOpenPreparedStatements); + druidDataSource.setValidationQuery(validationQuery); + druidDataSource.setTestWhileIdle(testWhileIdle); + druidDataSource.setTestOnBorrow(testOnBorrow); + druidDataSource.setTestOnReturn(testOnReturn); + return druidDataSource; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java new file mode 100644 index 00000000..e621b784 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.core.config; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import com.orangeforms.common.core.interceptor.MyRequestArgumentResolver; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyDateUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 所有的项目拦截器、参数解析器、消息对象转换器都在这里集中配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class CommonWebMvcConfig implements WebMvcConfigurer { + + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + // 添加MyRequestBody参数解析器 + argumentResolvers.add(new MyRequestArgumentResolver()); + } + + private HttpMessageConverter responseBodyConverter() { + return new StringHttpMessageConverter(StandardCharsets.UTF_8); + } + + @Bean + public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() { + FastJsonHttpMessageConverter fastConverter = new MyFastJsonHttpMessageConverter(); + List supportedMediaTypes = new ArrayList<>(); + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); + fastConverter.setSupportedMediaTypes(supportedMediaTypes); + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + fastJsonConfig.setSerializerFeatures( + SerializerFeature.PrettyFormat, + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.IgnoreNonFieldGetter); + fastJsonConfig.setDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + fastConverter.setFastJsonConfig(fastJsonConfig); + return fastConverter; + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(responseBodyConverter()); + converters.add(new ByteArrayHttpMessageConverter()); + converters.add(fastJsonHttpMessageConverter()); + } + + public static class MyFastJsonHttpMessageConverter extends FastJsonHttpMessageConverter { + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request == null) { + return super.canWrite(type, clazz, mediaType); + } + if (request.getRequestURI().contains("/v3/api-docs")) { + return false; + } + return super.canWrite(type, clazz, mediaType); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java new file mode 100644 index 00000000..b2bcabe2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * common-core的配置属性类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "common-core") +public class CoreProperties { + + public static final String MYSQL_TYPE = "mysql"; + public static final String POSTGRESQL_TYPE = "postgresql"; + public static final String ORACLE_TYPE = "oracle"; + public static final String DM_TYPE = "dm8"; + public static final String KINGBASE_TYPE = "kingbase"; + public static final String OPENGAUSS_TYPE = "opengauss"; + + /** + * 数据库类型。 + */ + private String databaseType = MYSQL_TYPE; + + /** + * 是否为MySQL。 + * + * @return 是返回true,否则false。 + */ + public boolean isMySql() { + return this.databaseType.equals(MYSQL_TYPE); + } + + /** + * 是否为PostgreSQl。 + * + * @return 是返回true,否则false。 + */ + public boolean isPostgresql() { + return this.databaseType.equals(POSTGRESQL_TYPE); + } + + /** + * 是否为Oracle。 + * + * @return 是返回true,否则false。 + */ + public boolean isOracle() { + return this.databaseType.equals(ORACLE_TYPE); + } + + /** + * 是否为达梦8。 + * + * @return 是返回true,否则false。 + */ + public boolean isDm() { + return this.databaseType.equals(DM_TYPE); + } + + /** + * 是否为人大金仓。 + * + * @return 是返回true,否则false。 + */ + public boolean isKingbase() { + return this.databaseType.equals(KINGBASE_TYPE); + } + + /** + * 是否为华为高斯。 + * + * @return 是返回true,否则false。 + */ + public boolean isOpenGauss() { + return this.databaseType.equals(OPENGAUSS_TYPE); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java new file mode 100644 index 00000000..534443d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.core.config; + +/** + * 通过线程本地存储的方式,保存当前数据库操作所需的数据源类型,动态数据源会根据该值,进行动态切换。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class DataSourceContextHolder { + + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源类型。 + * + * @param type 数据源类型 + * @return 原有数据源类型,如果第一次设置则返回null。 + */ + public static Integer setDataSourceType(Integer type) { + Integer datasourceType = CONTEXT_HOLDER.get(); + CONTEXT_HOLDER.set(type); + return datasourceType; + } + + /** + * 获取当前数据库操作执行线程的数据源类型,同时由动态数据源的路由函数调用。 + * + * @return 数据源类型。 + */ + public static Integer getDataSourceType() { + return CONTEXT_HOLDER.get(); + } + + /** + * 清除线程本地变量,以免内存泄漏。 + + * @param originalType 原有的数据源类型,如果该值为null,则情况本地化变量。 + */ + public static void unset(Integer originalType) { + if (originalType == null) { + CONTEXT_HOLDER.remove(); + } else { + CONTEXT_HOLDER.set(originalType); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataSourceContextHolder() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java new file mode 100644 index 00000000..8e03fcc2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.core.config; + +import lombok.Data; + +/** + * 主要用户动态多数据源使用的配置数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class DataSourceInfo { + /** + * 用于多数据源切换的数据源类型。 + */ + private Integer datasourceType; + /** + * 用户名。 + */ + private String username; + /** + * 密码。 + */ + private String password; + /** + * 数据库主机。 + */ + private String databaseHost; + /** + * 端口号。 + */ + private Integer port; + /** + * 模式名。 + */ + private String schemaName; + /** + * 数据库名称。 + */ + private String databaseName; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java new file mode 100644 index 00000000..1508412d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java @@ -0,0 +1,170 @@ +package com.orangeforms.common.core.config; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.util.Assert; + +import java.util.*; + +/** + * 动态数据源对象。当存在多个数据连接时使用。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class DynamicDataSource extends AbstractRoutingDataSource { + + @Autowired + private BaseMultiDataSourceConfig baseMultiDataSourceConfig; + @Autowired + private CoreProperties properties; + + private Set dynamicDatasourceTypeSet = new HashSet<>(); + private static final String ASSERT_MSG = "defaultTargetDatasource can't be null."; + + @Override + protected Object determineCurrentLookupKey() { + return DataSourceContextHolder.getDataSourceType(); + } + + /** + * 重新加载动态添加的数据源。既清空之前动态添加的数据源,同时添加参数中的新数据源列表。 + * + * @param dataSourceInfoList 新动态数据源列表。 + */ + public synchronized void reloadAll(List dataSourceInfoList) { + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + dynamicDatasourceTypeSet.forEach(dataSourceMap::remove); + dynamicDatasourceTypeSet.clear(); + for (DataSourceInfo dataSourceInfo : dataSourceInfoList) { + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + } + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 添加动态添加数据源。 + * + * 动态添加数据源。 + */ + public synchronized void addDataSource(DataSourceInfo dataSourceInfo) { + if (dynamicDatasourceTypeSet.contains(dataSourceInfo.getDatasourceType())) { + return; + } + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 添加动态添加数据源列表。 + * + * @param dataSourceInfoList 数据源信息列表。 + */ + public synchronized void addDataSources(List dataSourceInfoList) { + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + for (DataSourceInfo dataSourceInfo : dataSourceInfoList) { + if (!dynamicDatasourceTypeSet.contains(dataSourceInfo.getDatasourceType())) { + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + } + } + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 动态移除数据源。 + * + * @param datasourceType 数据源类型。 + */ + public synchronized void removeDataSource(int datasourceType) { + if (!dynamicDatasourceTypeSet.remove(datasourceType)) { + return; + } + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + dataSourceMap.remove(datasourceType); + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + private DruidDataSource doConvert(DataSourceInfo dataSourceInfo) { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + dataSource.setUsername(dataSourceInfo.getUsername()); + dataSource.setPassword(dataSourceInfo.getPassword()); + StringBuilder urlBuilder = new StringBuilder(256); + String hostAndPort = dataSourceInfo.getDatabaseHost() + ":" + dataSourceInfo.getPort(); + if (properties.isMySql()) { + urlBuilder.append("jdbc:mysql://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()) + .append("?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"); + } else if (properties.isOracle()) { + urlBuilder.append("jdbc:oracle:thin:@") + .append(hostAndPort) + .append(":") + .append(dataSourceInfo.getDatabaseName()); + } else if (properties.isPostgresql()) { + urlBuilder.append("jdbc:postgresql://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()); + if (StrUtil.isBlank(dataSourceInfo.getSchemaName())) { + urlBuilder.append("?currentSchema=public"); + } else { + urlBuilder.append("?currentSchema=").append(dataSourceInfo.getSchemaName()); + } + urlBuilder.append("&TimeZone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8"); + } else if (properties.isDm()) { + urlBuilder.append("jdbc:dm://") + .append(hostAndPort) + .append("?schema=") + .append(dataSourceInfo.getDatabaseName()) + .append("&useJDBCCompliantTimezoneShift=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8"); + } else if (properties.isKingbase()) { + urlBuilder.append("jdbc:kingbase8://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()) + .append("?useJDBCCompliantTimezoneShift=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8"); + } else if (properties.isOpenGauss()) { + urlBuilder.append("jdbc:opengauss://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()); + if (StrUtil.isBlank(dataSourceInfo.getSchemaName())) { + urlBuilder.append("?currentSchema=public"); + } else { + urlBuilder.append("?currentSchema=").append(dataSourceInfo.getSchemaName()); + } + } + dataSource.setUrl(urlBuilder.toString()); + return dataSource; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java new file mode 100644 index 00000000..830199b7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * 目前用于用户密码加密,UAA接入应用客户端的client_secret加密。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class EncryptConfig { + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/PageHelperConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/PageHelperConfig.java new file mode 100644 index 00000000..6f46bc4e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/PageHelperConfig.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.core.config; + +import com.github.pagehelper.PageInterceptor; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +/** + * pagehelper的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "pagehelper") +public class PageHelperConfig { + + private String helperDialect; + private String reasonable; + private String supportMethodsArguments; + private String params; + + @Bean + public PageInterceptor pageInterceptor() { + PageInterceptor interceptor = new PageInterceptor(); + Properties p = new Properties(); + p.setProperty("helperDialect", helperDialect); + p.setProperty("reasonable", reasonable); + p.setProperty("supportMethodsArguments", supportMethodsArguments); + p.setProperty("params", params); + interceptor.setProperties(p); + return interceptor; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java new file mode 100644 index 00000000..d8deb0ad --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.core.config; + +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * RestTemplate连接池配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class RestTemplateConfig { + private static final int MAX_TOTAL_CONNECTION = 50; + private static final int MAX_CONNECTION_PER_ROUTE = 20; + private static final int CONNECTION_TIMEOUT = 20000; + private static final int READ_TIMEOUT = 30000; + + @Bean + @ConditionalOnMissingBean({RestOperations.class, RestTemplate.class}) + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(createFactory()); + List> messageConverters = restTemplate.getMessageConverters(); + messageConverters.removeIf( + c -> c instanceof StringHttpMessageConverter || c instanceof MappingJackson2HttpMessageConverter); + messageConverters.add(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); + messageConverters.add(new FastJsonHttpMessageConverter()); + restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + public void handleError(ClientHttpResponse response) throws IOException { + // 防止400+和500等错误被直接抛出异常,这里避开了缺省处理方式,所有的错误均交给业务代码处理。 + } + }); + return restTemplate; + } + + private ClientHttpRequestFactory createFactory() { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(MAX_TOTAL_CONNECTION); + connectionManager.setDefaultMaxPerRoute(MAX_CONNECTION_PER_ROUTE); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(CONNECTION_TIMEOUT, TimeUnit.MICROSECONDS) + .setResponseTimeout(READ_TIMEOUT, TimeUnit.MICROSECONDS) + .build(); + HttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(connectionManager) + .build(); + return new HttpComponentsClientHttpRequestFactory(httpClient); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java new file mode 100644 index 00000000..90ed08fd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.core.config; + +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * tomcat配置对象。当前配置禁用了PUT和DELETE方法,防止渗透攻击。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class TomcatConfig { + + @Bean + public TomcatServletWebServerFactory servletContainer() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.addContextCustomizers(context -> { + SecurityConstraint securityConstraint = new SecurityConstraint(); + securityConstraint.setUserConstraint("CONFIDENTIAL"); + SecurityCollection collection = new SecurityCollection(); + collection.addPattern("/*"); + collection.addMethod("HEAD"); + collection.addMethod("PUT"); + collection.addMethod("PATCH"); + collection.addMethod("DELETE"); + collection.addMethod("TRACE"); + collection.addMethod("COPY"); + collection.addMethod("SEARCH"); + collection.addMethod("PROPFIND"); + securityConstraint.addCollection(collection); + context.addConstraint(securityConstraint); + }); + return factory; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java new file mode 100644 index 00000000..d0368de0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 聚合计算的常量类型对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class AggregationType { + + /** + * sum 计数 + */ + public static final int SUM = 0; + /** + * count 汇总 + */ + public static final int COUNT = 1; + /** + * average 平均值 + */ + public static final int AVG = 2; + /** + * min 最小值 + */ + public static final int MIN = 3; + /** + * max 最大值 + */ + public static final int MAX = 4; + + private static final Map DICT_MAP = new HashMap<>(5); + static { + DICT_MAP.put(SUM, "累计总和"); + DICT_MAP.put(COUNT, "数量总和"); + DICT_MAP.put(AVG, "平均值"); + DICT_MAP.put(MIN, "最小值"); + DICT_MAP.put(MAX, "最大值"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 获取与SQL对应的聚合函数字符串名称。 + * + * @return 聚合函数名称。 + */ + public static String getAggregationFunction(Integer aggregationType) { + switch (aggregationType) { + case COUNT: + return "COUNT"; + case AVG: + return "AVG"; + case SUM: + return "SUM"; + case MAX: + return "MAX"; + case MIN: + return "MIN"; + default: + throw new IllegalArgumentException("无效的聚合类型!"); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AggregationType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java new file mode 100644 index 00000000..edad8271 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * App 登录的设备类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class AppDeviceType { + + /** + * 移动端 (如果不考虑区分android或ios的,可以使用该值) + */ + public static final int MOBILE = 0; + /** + * android + */ + public static final int ANDROID = 1; + /** + * iOS + */ + public static final int IOS = 2; + /** + * 微信公众号和小程序 + */ + public static final int WEIXIN = 3; + /** + * PC WEB + */ + public static final int WEB = 4; + + private static final Map DICT_MAP = new HashMap<>(5); + static { + DICT_MAP.put(MOBILE, "Mobile"); + DICT_MAP.put(ANDROID, "Android"); + DICT_MAP.put(IOS, "iOS"); + DICT_MAP.put(WEIXIN, "Wechat"); + DICT_MAP.put(WEB, "WEB"); + } + + /** + * 根据设备类型返回设备名称。 + * + * @param deviceType 设备类型。 + * @return 设备名称。 + */ + public static String getDeviceTypeName(int deviceType) { + return DICT_MAP.get(deviceType); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AppDeviceType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java new file mode 100644 index 00000000..25fce820 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java @@ -0,0 +1,161 @@ +package com.orangeforms.common.core.constant; + +import java.util.regex.Pattern; + +/** + * 应用程序的常量声明对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class ApplicationConstant { + + /** + * 适用于所有类型的字典格式数据。该常量为字典的键字段。 + */ + public static final String DICT_ID = "id"; + /** + * 适用于所有类型的字典格式数据。该常量为字典的名称字段。 + */ + public static final String DICT_NAME = "name"; + /** + * 适用于所有类型的字典格式数据。该常量为字典的键父字段。 + */ + public static final String PARENT_ID = "parentId"; + /** + * 数据同步使用的缺省消息队列主题名称。 + */ + public static final String DEFAULT_DATA_SYNC_TOPIC = "OrangeFormsOpen"; + /** + * 全量数据同步中,新增数据对象的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_DATA_KEY = "data"; + /** + * 全量数据同步中,原有数据对象的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_OLD_DATA_KEY = "oldData"; + /** + * 全量数据同步中,数据对象主键的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_ID_KEY = "id"; + /** + * 为字典表数据缓存时,缓存名称的固定后缀。 + */ + public static final String DICT_CACHE_NAME_SUFFIX = "-DICT"; + /** + * 为树形字典表数据缓存时,缓存名称的固定后缀。 + */ + public static final String TREE_DICT_CACHE_NAME_SUFFIX = "-TREE-DICT"; + /** + * 图片文件上传的父目录。 + */ + public static final String UPLOAD_IMAGE_PARENT_PATH = "image"; + /** + * 附件文件上传的父目录。 + */ + public static final String UPLOAD_ATTACHMENT_PARENT_PATH = "attachment"; + /** + * CSV文件扩展名。 + */ + public static final String CSV_EXT = "csv"; + /** + * XLSX文件扩展名。 + */ + public static final String XLSX_EXT = "xlsx"; + /** + * 统计分类计算时,按天聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String DAY_AGGREGATION = "day"; + /** + * 统计分类计算时,按月聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String MONTH_AGGREGATION = "month"; + /** + * 统计分类计算时,按年聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String YEAR_AGGREGATION = "year"; + /** + * 请求头跟踪id名。 + */ + public static final String HTTP_HEADER_TRACE_ID = "traceId"; + /** + * 请求头菜单Id。 + */ + public static final String HTTP_HEADER_MENU_ID = "MenuId"; + /** + * 数据权限中,标记所有菜单的Id值。 + */ + public static final String DATA_PERM_ALL_MENU_ID = "AllMenuId"; + /** + * 请求头中记录的原始请求URL。 + */ + public static final String HTTP_HEADER_ORIGINAL_REQUEST_URL = "MY_ORIGINAL_REQUEST_URL"; + /** + * 免登录验证接口的请求头key。 + */ + public static final String HTTP_HEADER_DONT_AUTH = "DONT_AUTH"; + /** + * 系统服务内部调用时,可使用该HEAD,以便和外部调用加以区分,便于监控和流量分析。 + */ + public static final String HTTP_HEADER_INTERNAL_TOKEN = "INTERNAL_AUTH_TOKEN"; + /** + * 操作日志的数据源类型。 + */ + public static final int OPERATION_LOG_DATASOURCE_TYPE = 1000; + /** + * 在线表单的数据源类型。 + */ + public static final int COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE = 1010; + /** + * 报表模块的数据源类型。 + */ + public static final int COMMON_REPORT_DATASOURCE_TYPE = 1020; + /** + * 全局编码字典的数据源类型。 + */ + public static final int COMMON_GLOBAL_DICT_TYPE = 1050; + /** + * 租户管理所对应的数据源常量值。 + */ + public static final int TENANT_ADMIN_DATASOURCE_TYPE = 1100; + /** + * 租户业务默认数据库(系统搭建时的第一个租户数据库)所对应的数据源常量值。 + */ + public static final int TENANT_BUSINESS_DATASOURCE_TYPE = 1120; + /** + * 租户通用数据所对应的数据源常量值,如全局编码字典、在线表单、流程和报表等内置表数据。 + */ + public static final int TENANT_COMMON_DATASOURCE_TYPE = 1130; + /** + * 租户动态数据源主题(Redis)。 + */ + public static final String TENANT_DYNAMIC_DATASOURCE_TOPIC = "TenantDynamicDatasoruce"; + /** + * 租户基础数据同步(RocketMQ),如upms、全局编码字典、在线表单、流程、报表等。 + */ + public static final String TENANT_DATASYNC_TOPIC = "TenantSync"; + /** + * 租户管理的应用名。 + */ + public static final String TENANT_ADMIN_APP_NAME = "tenant-admin"; + /** + * 重要说明:该值为项目生成后的缺省密钥,仅为使用户可以快速上手并跑通流程。 + * 在实际的应用中,一定要为不同的项目或服务,自行生成公钥和私钥,并将 PRIVATE_KEY 的引用改为服务的配置项。 + * 密钥的生成方式,可通过执行common.core.util.RsaUtil类的main函数动态生成。 + */ + public static final String PRIVATE_KEY = + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKkLhAydtOtA4WuIkkIIUVaGWu4ElOEAQF9GTulHHWOwCHI1UvcKolvS1G+mdsKcmGtEAQ92AUde/kDRGu8Wn7kLDtCgUfo72soHz7Qfv5pVB4ohMxQd/9cxeKjKbDoirhB9Z3xGF20zUozp4ZPLxpTtI7azr0xzUtd5+D/HfLDrAgMBAAECgYEApESZhDz4YyeAJiPnpJ06lS8oS2VOWzsIUs0av5uoloeoHXtt7Lx7u2kroHeNrl3Hy2yg7ypH4dgQkGHin3VHrVAgjG3TxhgBXIqqntzzk2AGJKBeIIkRX86uTvtKZyp3flUgcwcGmpepAHS1V1DPY3aVYvbcqAmoL6DX6VYN0NECQQDQUitMdC76lEtAr5/ywS0nrZJDo6U7eQ7ywx/eiJ+YmrSye8oorlAj1VBWG+Cl6jdHOHtTQyYv/tu71fjzQiJTAkEAz7wb47/vcSUpNWQxItFpXz0o6rbJh71xmShn1AKP7XptOVZGlW9QRYEzHabV9m/DHqI00cMGhHrWZAhCiTkUCQJAFsJjaJ7o4weAkTieyO7B+CvGZw1h5/V55Jvcx3s1tH5yb22G0Jr6tm9/r2isSnQkReutzZLwgR3e886UvD7lcQJAAUcD2OOuQkDbPwPNtYwaHMbQgJj9JkOI9kskUE5vuiMdltOr/XFAyhygRtdmy2wmhAK1VnDfkmL6/IR8fEGImQJABOB0KCalb0M8CPnqqHzozrD8gPObnIIr4aVvLIPATN2g7MM2N6F7JbI4RZFiKa92LV6bhQCY8OvHi5K2cgFpbw=="; + /** + * SQL注入检测的正则对象。 + */ + @SuppressWarnings("all") + public static final Pattern SQL_INJECT_PATTERN = + Pattern.compile("(.*\\=.*\\-\\-.*)|(.*(\\+).*)|(.*\\w+(%|\\$|#|&)\\w+.*)|(.*\\|\\|.*)|(.*\\s+(and|or)\\s+.*)" + + "|(.*\\b(select|update|union|and|or|delete|insert|trancate|char|substr|ascii|declare|exec|count|master|into|drop|execute|sleep|extractvalue|updatexml|substring|database|concat|rand)\\b.*)"); + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ApplicationConstant() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java new file mode 100644 index 00000000..772d0597 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据权限规则类型常量类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DataPermRuleType { + + /** + * 查看全部。 + */ + public static final int TYPE_ALL = 0; + + /** + * 仅查看当前用户。 + */ + public static final int TYPE_USER_ONLY = 1; + + /** + * 仅查看当前部门。 + */ + public static final int TYPE_DEPT_ONLY = 2; + + /** + * 所在部门及子部门。 + */ + public static final int TYPE_DEPT_AND_CHILD_DEPT = 3; + + /** + * 多部门及子部门。 + */ + public static final int TYPE_MULTI_DEPT_AND_CHILD_DEPT = 4; + + /** + * 自定义部门列表。 + */ + public static final int TYPE_CUSTOM_DEPT_LIST = 5; + + /** + * 本部门所有用户。 + */ + public static final int TYPE_DEPT_USERS = 6; + + /** + * 本部门及子部门所有用户。 + */ + public static final int TYPE_DEPT_AND_CHILD_DEPT_USERS = 7; + + private static final Map DICT_MAP = new HashMap<>(6); + static { + DICT_MAP.put(TYPE_ALL, "查看全部"); + DICT_MAP.put(TYPE_USER_ONLY, "仅查看当前用户"); + DICT_MAP.put(TYPE_DEPT_ONLY, "仅查看所在部门"); + DICT_MAP.put(TYPE_DEPT_AND_CHILD_DEPT, "所在部门及子部门"); + DICT_MAP.put(TYPE_MULTI_DEPT_AND_CHILD_DEPT, "多部门及子部门"); + DICT_MAP.put(TYPE_CUSTOM_DEPT_LIST, "自定义部门列表"); + DICT_MAP.put(TYPE_DEPT_USERS, "本部门所有用户"); + DICT_MAP.put(TYPE_DEPT_AND_CHILD_DEPT_USERS, "本部门及子部门所有用户"); + } + + /** + * 判断参数是否为当前常量字典的合法取值范围。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataPermRuleType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java new file mode 100644 index 00000000..5d294431 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字典类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DictType { + + /** + * 数据表字典。 + */ + public static final int TABLE = 1; + /** + * URL字典。 + */ + public static final int URL = 5; + /** + * 常量字典。 + */ + public static final int CONST = 10; + /** + * 自定义字典。 + */ + public static final int CUSTOM = 15; + /** + * 全局编码字典。 + */ + public static final int GLOBAL_DICT = 20; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(TABLE, "数据表字典"); + DICT_MAP.put(URL, "URL字典"); + DICT_MAP.put(CONST, "静态字典"); + DICT_MAP.put(CUSTOM, "自定义字典"); + DICT_MAP.put(GLOBAL_DICT, "全局编码字典"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DictType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java new file mode 100644 index 00000000..423ba928 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java @@ -0,0 +1,88 @@ +package com.orangeforms.common.core.constant; + +/** + * 返回应答中的错误代码和错误信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +public enum ErrorCodeEnum { + + /** + * 没有错误 + */ + NO_ERROR("没有错误"), + /** + * 未处理的异常! + */ + UNHANDLED_EXCEPTION("未处理的异常!"), + + ARGUMENT_NULL_EXIST("数据验证失败,接口调用参数存在空值,请核对!"), + ARGUMENT_PK_ID_NULL("数据验证失败,接口调用主键Id参数为空,请核对!"), + INVALID_ARGUMENT_FORMAT("数据验证失败,不合法的参数格式,请核对!"), + INVALID_STATUS_ARGUMENT("数据验证失败,无效的状态参数值,请核对!"), + UPLOAD_FAILED("数据验证失败,数据上传失败!"), + INVALID_UPLOAD_FIELD("数据验证失败,该字段不支持数据上传!"), + INVALID_UPLOAD_STORE_TYPE("数据验证失败,并不支持上传存储类型!"), + INVALID_UPLOAD_FILE_ARGUMENT("数据验证失败,上传文件参数错误,请核对!"), + INVALID_UPLOAD_FILE_FORMAT("无效的上传文件格式!"), + INVALID_UPLOAD_FILE_IOERROR("上传文件写入失败,请联系管理员!"), + UNAUTHORIZED_LOGIN("当前用户尚未登录或登录已超时,请重新登录!"), + UNAUTHORIZED_USER_PERMISSION("权限验证失败,当前用户不能访问该接口,请核对!"), + NO_ACCESS_PERMISSION("当前用户没有访问权限,请核对!"), + NO_OPERATION_PERMISSION("当前用户没有操作权限,请核对!"), + + PASSWORD_ERR("密码错误,请重试!"), + INVALID_USERNAME_PASSWORD("用户名或密码错误,请重试!"), + INVALID_ACCESS_TOKEN("无效的用户访问令牌!"), + INVALID_USER_STATUS("用户状态错误,请刷新后重试!"), + INVALID_TENANT_CODE("指定的租户编码并不存在,请刷新后重试!"), + INVALID_TENANT_STATUS("当前租户为不可用状态,请刷新后重试!"), + INVALID_USER_TENANT("当前用户并不属于当前租户,请刷新后重试!"), + + HAS_CHILDREN_DATA("数据验证失败,子数据存在,请刷新后重试!"), + DATA_VALIDATED_FAILED("数据验证失败,请核对!"), + UPLOAD_FILE_FAILED("文件上传失败,请联系管理员!"), + DATA_SAVE_FAILED("数据保存失败,请联系管理员!"), + DATA_ACCESS_FAILED("数据访问失败,请联系管理员!"), + DATA_PERM_ACCESS_FAILED("数据访问失败,您没有该页面的数据访问权限!"), + DUPLICATED_UNIQUE_KEY("数据保存失败,存在重复数据,请核对!"), + DATA_NOT_EXIST("数据不存在,请刷新后重试!"), + DATA_PARENT_LEVEL_ID_NOT_EXIST("数据验证失败,父级别关联Id不存在,请刷新后重试!"), + DATA_PARENT_ID_NOT_EXIST("数据验证失败,ParentId不存在,请核对!"), + INVALID_RELATED_RECORD_ID("数据验证失败,关联数据并不存在,请刷新后重试!"), + INVALID_DATA_MODEL("数据验证失败,无效的数据实体对象!"), + INVALID_DATA_FIELD("数据验证失败,无效的数据实体对象字段!"), + INVALID_CLASS_FIELD("数据验证失败,无效的类对象字段!"), + SERVER_INTERNAL_ERROR("服务器内部错误,请联系管理员!"), + REDIS_CACHE_ACCESS_TIMEOUT("Redis缓存数据访问超时,请刷新后重试!"), + REDIS_CACHE_ACCESS_STATE_ERROR("Redis缓存数据访问状态错误,请刷新后重试!"), + FAILED_TO_INVOKE_THIRDPARTY_URL("调用第三方接口失败!"), + + FLOW_WORK_ORDER_EXIST("该业务数据Id存在尚未完成审批的流程实例,同一业务数据主键不能同时重复提交审批!"); + + // 下面的枚举值为特定枚举值,即开发者可以根据自己的项目需求定义更多的非通用枚举值 + + /** + * 构造函数。 + * + * @param errorMessage 错误消息。 + */ + ErrorCodeEnum(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * 错误信息。 + */ + private final String errorMessage; + + /** + * 获取错误信息。 + * + * @return 错误信息。 + */ + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java new file mode 100644 index 00000000..db0e1752 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java @@ -0,0 +1,127 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FieldFilterType { + /** + * 等于过滤。 + */ + public static final int EQUAL = 0; + /** + * 不等于过滤。 + */ + public static final int NOT_EQUAL = 1; + /** + * 大于等于。 + */ + public static final int GE = 2; + /** + * 大于。 + */ + public static final int GT = 3; + /** + * 小于等于。 + */ + public static final int LE = 4; + /** + * 小于。 + */ + public static final int LT = 5; + /** + * 模糊查询。 + */ + public static final int LIKE = 6; + /** + * IN列表过滤。 + */ + public static final int IN = 7; + /** + * NOT IN列表过滤。 + */ + public static final int NOT_IN = 8; + /** + * 范围过滤。 + */ + public static final int BETWEEN = 9; + /** + * 不为空。 + */ + public static final int IS_NOT_NULL = 100; + /** + * 为空。 + */ + public static final int IS_NULL = 101; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(EQUAL, " = "); + DICT_MAP.put(NOT_EQUAL, " <> "); + DICT_MAP.put(GE, " >= "); + DICT_MAP.put(GT, " > "); + DICT_MAP.put(LE, " <= "); + DICT_MAP.put(LT, " < "); + DICT_MAP.put(LIKE, " LIKE "); + DICT_MAP.put(IN, " IN "); + DICT_MAP.put(NOT_IN, " NOT IN "); + DICT_MAP.put(BETWEEN, " BETWEEN "); + DICT_MAP.put(IS_NOT_NULL, " IS NOT NULL "); + DICT_MAP.put(IS_NULL, " IS NULL "); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 获取显示名。 + * @param value 常量值。 + * @return 常量值对应的显示名。 + */ + public static String getName(Integer value) { + return DICT_MAP.get(value); + } + + /** + * 不支持日期型字段的过滤类型。 + * + * @param filterType 过滤类型。 + * @return 不支持返回true,否则false。 + */ + public static boolean unsupportDateFilterType(int filterType) { + return filterType == FieldFilterType.IN + || filterType == FieldFilterType.NOT_IN + || filterType == FieldFilterType.NOT_EQUAL + || filterType == FieldFilterType.LIKE; + } + + /** + * 支持多过滤值的过滤类型。 + * + * @param filterType 过滤类型。 + * @return 支持返回true,否则false。 + */ + public static boolean supportMultiValueFilterType(int filterType) { + return filterType == FieldFilterType.IN + || filterType == FieldFilterType.NOT_IN + || filterType == FieldFilterType.BETWEEN; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldFilterType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java new file mode 100644 index 00000000..dda91b2e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤参数类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FilterParamType { + + /** + * 整数数值型。 + */ + public static final int LONG = 0; + /** + * 浮点型。 + */ + public static final int FLOAT = 1; + /** + * 字符型。 + */ + public static final int STRING = 2; + /** + * 日期型。 + */ + public static final int DATE = 3; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(LONG, "整数数值型"); + DICT_MAP.put(FLOAT, "浮点型"); + DICT_MAP.put(STRING, "字符型"); + DICT_MAP.put(DATE, "日期型"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FilterParamType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java new file mode 100644 index 00000000..a7ed6ba3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.core.constant; + +/** + * 数据记录逻辑删除标记常量。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class GlobalDeletedFlag { + + /** + * 表示数据表记录已经删除 + */ + public static final int DELETED = -1; + /** + * 数据记录正常 + */ + public static final int NORMAL = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalDeletedFlag() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java new file mode 100644 index 00000000..d242e26c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.core.constant; + +/** + * 字段脱敏类型枚举。。 + * + * @author Jerry + * @date 2024-07-02 + */ +public enum MaskFieldTypeEnum { + + /** + * 自定义实现。 + */ + CUSTOM, + /** + * 姓名。 + */ + NAME, + /** + * 移动电话。 + */ + MOBILE_PHONE, + /** + * 座机电话。 + */ + FIXED_PHONE, + /** + * 身份证。 + */ + ID_CARD, + /** + * 银行卡号。 + */ + BANK_CARD, + /** + * 汽车牌照号。 + */ + CAR_LICENSE, + /** + * 邮件。 + */ + EMAIL, + /** + * 固定长度的前缀和后缀不被掩码。 + */ + NO_MASK_PREFIX_SUFFIX, +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java new file mode 100644 index 00000000..660b606c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.constant; + +/** + * 对应于数据表字段中的类型,我们需要统一映射到Java实体对象字段的类型。 + * 该类是描述Java实体对象字段类型的常量类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class ObjectFieldType { + + public static final String LONG = "Long"; + public static final String INTEGER = "Integer"; + public static final String DOUBLE = "Double"; + public static final String BIG_DECIMAL = "BigDecimal"; + public static final String BOOLEAN = "Boolean"; + public static final String STRING = "String"; + public static final String DATE = "Date"; + public static final String BYTE_ARRAY = "byte[]"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ObjectFieldType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java new file mode 100644 index 00000000..d966cf6d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.core.constant; + +/** + * 用户分组过滤常量。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class UserFilterGroup { + + public static final String USER = "USER_GROUP"; + public static final String ROLE = "ROLE_GROUP"; + public static final String DEPT = "DEPT_GROUP"; + public static final String POST = "POST_GROUP"; + public static final String DEPT_POST = "DEPT_POST_GROUP"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private UserFilterGroup() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java new file mode 100644 index 00000000..66053ad5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.exception; + +/** + * 数据验证失败的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class DataValidationException extends RuntimeException { + + /** + * 构造函数。 + */ + public DataValidationException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public DataValidationException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java new file mode 100644 index 00000000..762eac91 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java @@ -0,0 +1,30 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的类对象字段的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidClassFieldException extends RuntimeException { + + private final String className; + private final String fieldName; + + /** + * 构造函数。 + * + * @param className 对象名。 + * @param fieldName 字段名。 + */ + public InvalidClassFieldException(String className, String fieldName) { + super("Invalid FieldName [" + fieldName + "] in Class [" + className + "]."); + this.className = className; + this.fieldName = fieldName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java new file mode 100644 index 00000000..2c5d249e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java @@ -0,0 +1,30 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的实体对象字段的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDataFieldException extends RuntimeException { + + private final String modelName; + private final String fieldName; + + /** + * 构造函数。 + * + * @param modelName 实体对象名。 + * @param fieldName 字段名。 + */ + public InvalidDataFieldException(String modelName, String fieldName) { + super("Invalid FieldName [" + fieldName + "] in Model Class [" + modelName + "]."); + this.modelName = modelName; + this.fieldName = fieldName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java new file mode 100644 index 00000000..b17abb8e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的实体对象的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDataModelException extends RuntimeException { + + private final String modelName; + + /** + * 构造函数。 + * + * @param modelName 实体对象名。 + */ + public InvalidDataModelException(String modelName) { + super("Invalid Model Class [" + modelName + "]."); + this.modelName = modelName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java new file mode 100644 index 00000000..b7589219 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的数据库链接类型自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDblinkTypeException extends RuntimeException { + + /** + * 构造函数。 + * + * @param dblinkType 数据库链接类型。 + */ + public InvalidDblinkTypeException(int dblinkType) { + super("Invalid Dblink Type [" + dblinkType + "]."); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java new file mode 100644 index 00000000..9b197625 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的Redis模式的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidRedisModeException extends RuntimeException { + + private final String mode; + + /** + * 构造函数。 + * + * @param mode 错误的模式。 + */ + public InvalidRedisModeException(String mode) { + super("Invalid Redis Mode [" + mode + "], only supports [single/cluster/sentinel/master_slave]"); + this.mode = mode; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java new file mode 100644 index 00000000..b47dd010 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.core.exception; + +/** + * 内存缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MapCacheAccessException extends RuntimeException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param cause 原始异常。 + */ + public MapCacheAccessException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java new file mode 100644 index 00000000..82d8f4ae --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java @@ -0,0 +1,46 @@ +package com.orangeforms.common.core.exception; + +/** + * 自定义的运行时异常,在需要抛出运行时异常时,可使用该异常。 + * NOTE:主要是为了避免SonarQube进行代码质量扫描时,给出警告。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyRuntimeException extends RuntimeException { + + /** + * 构造函数。 + */ + public MyRuntimeException() { + + } + + /** + * 构造函数。 + * + * @param throwable 引发异常对象。 + */ + public MyRuntimeException(Throwable throwable) { + super(throwable); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public MyRuntimeException(String msg) { + super(msg); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param throwable 引发异常对象。 + */ + public MyRuntimeException(String msg, Throwable throwable) { + super(msg, throwable); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java new file mode 100644 index 00000000..0d9dd3d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.exception; + +/** + * 没有数据被修改的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class NoDataAffectException extends RuntimeException { + + /** + * 构造函数。 + */ + public NoDataAffectException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public NoDataAffectException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java new file mode 100644 index 00000000..2e18d311 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.exception; + +/** + * 没有数据访问权限的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class NoDataPermException extends RuntimeException { + + /** + * 构造函数。 + */ + public NoDataPermException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public NoDataPermException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java new file mode 100644 index 00000000..b0dfe017 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.core.exception; + +/** + * Redis缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class RedisCacheAccessException extends RuntimeException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param cause 原始异常。 + */ + public RedisCacheAccessException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java new file mode 100644 index 00000000..08c198ad --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java @@ -0,0 +1,227 @@ +package com.orangeforms.common.core.interceptor; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import org.apache.commons.io.IOUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.util.*; + +/** + * MyRequestBody解析器 + * 解决的问题: + * 1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收; + * 2、多个对象需要封装到一个对象里才可以用@RequestBody接收。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyRequestArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String JSONBODY_ATTRIBUTE = "MY_REQUEST_BODY_ATTRIBUTE_XX"; + + private static final Set> CLASS_SET = new HashSet<>(); + + static { + CLASS_SET.add(Integer.class); + CLASS_SET.add(Long.class); + CLASS_SET.add(Short.class); + CLASS_SET.add(Float.class); + CLASS_SET.add(Double.class); + CLASS_SET.add(Boolean.class); + CLASS_SET.add(Byte.class); + CLASS_SET.add(BigDecimal.class); + CLASS_SET.add(Character.class); + CLASS_SET.add(Date.class); + } + + /** + * 设置支持的方法参数类型。 + * + * @param parameter 方法参数。 + * @return 支持的类型。 + */ + @Override + public boolean supportsParameter(@NonNull MethodParameter parameter) { + return parameter.hasParameterAnnotation(MyRequestBody.class); + } + + /** + * 参数解析,利用fastjson。 + * 注意:非基本类型返回null会报空指针异常,要通过反射或者JSON工具类创建一个空对象。 + */ + @Override + public Object resolveArgument( + @NonNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + @NonNull NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.notNull(servletRequest, "HttpServletRequest can't be NULL."); + String contentType = servletRequest.getContentType(); + if (!HttpMethod.POST.name().equals(servletRequest.getMethod())) { + throw new IllegalArgumentException("Only POST method can be applied @MyRequestBody annotation!"); + } + if (!StrUtil.containsIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE)) { + throw new IllegalArgumentException( + "Only application/json Content-Type can be applied @MyRequestBody annotation!"); + } + // 根据@MyRequestBody注解value作为json解析的key + MyRequestBody parameterAnnotation = parameter.getParameterAnnotation(MyRequestBody.class); + Assert.notNull(parameterAnnotation, "parameterAnnotation can't be NULL"); + JSONObject jsonObject = getRequestBody(webRequest); + if (jsonObject == null) { + if (parameterAnnotation.required()) { + throw new IllegalArgumentException("Request Body is EMPTY!"); + } + return null; + } + String key = parameterAnnotation.value(); + if (StrUtil.isBlank(key)) { + key = parameter.getParameterName(); + } + Object value = jsonObject.get(key); + if (value == null) { + if (parameterAnnotation.required()) { + throw new IllegalArgumentException(String.format("Required parameter %s is not present!", key)); + } + return null; + } + // 获取参数类型。 + Class parameterType = parameter.getParameterType(); + // 基本类型 + if (parameterType.isPrimitive()) { + return parsePrimitive(parameterType.getName(), value); + } + // 基本类型包装类 + if (isBasicDataTypes(parameterType)) { + return parseBasicTypeWrapper(parameterType, value); + } else if (parameterType == String.class) { + // 字符串类型 + return value.toString(); + } + // 对象类型 + if (!(value instanceof JSONArray)) { + // 其他复杂对象 + return JSON.toJavaObject((JSONObject) value, parameterType); + } + if (parameter.getGenericParameterType() instanceof ParameterizedType) { + return ((JSONArray) value).toJavaObject(parameter.getGenericParameterType()); + } + // 非参数化的集合类型 + return JSON.parseObject(value.toString(), parameterType); + } + + private Object parsePrimitive(String parameterTypeName, Object value) { + final String booleanTypeName = "boolean"; + if (booleanTypeName.equals(parameterTypeName)) { + return Boolean.valueOf(value.toString()); + } + final String intTypeName = "int"; + if (intTypeName.equals(parameterTypeName)) { + return Integer.valueOf(value.toString()); + } + final String charTypeName = "char"; + if (charTypeName.equals(parameterTypeName)) { + return value.toString().charAt(0); + } + final String shortTypeName = "short"; + if (shortTypeName.equals(parameterTypeName)) { + return Short.valueOf(value.toString()); + } + final String longTypeName = "long"; + if (longTypeName.equals(parameterTypeName)) { + return Long.valueOf(value.toString()); + } + final String floatTypeName = "float"; + if (floatTypeName.equals(parameterTypeName)) { + return Float.valueOf(value.toString()); + } + final String doubleTypeName = "double"; + if (doubleTypeName.equals(parameterTypeName)) { + return Double.valueOf(value.toString()); + } + final String byteTypeName = "byte"; + if (byteTypeName.equals(parameterTypeName)) { + return Byte.valueOf(value.toString()); + } + return null; + } + + private Object parseBasicTypeWrapper(Class parameterType, Object value) { + if (Number.class.isAssignableFrom(parameterType)) { + return this.parseNumberType(parameterType, value); + } else if (parameterType == Boolean.class) { + return value; + } else if (parameterType == Character.class) { + return value.toString().charAt(0); + } else if (parameterType == Date.class) { + return Convert.toDate(value); + } + return null; + } + + private Object parseNumberType(Class parameterType, Object value) { + if (value instanceof String) { + return Convert.convert(parameterType, value); + } + Number number = (Number) value; + if (parameterType == Integer.class) { + return number.intValue(); + } else if (parameterType == Short.class) { + return number.shortValue(); + } else if (parameterType == Long.class) { + return number.longValue(); + } else if (parameterType == Float.class) { + return number.floatValue(); + } else if (parameterType == Double.class) { + return number.doubleValue(); + } else if (parameterType == Byte.class) { + return number.byteValue(); + } else if (parameterType == BigDecimal.class) { + if (value instanceof Double || value instanceof Float) { + return BigDecimal.valueOf(number.doubleValue()); + } else { + return BigDecimal.valueOf(number.longValue()); + } + } + return null; + } + + private boolean isBasicDataTypes(Class clazz) { + return CLASS_SET.contains(clazz); + } + + private JSONObject getRequestBody(NativeWebRequest webRequest) throws IOException { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.notNull(servletRequest, "servletRequest can't be NULL"); + // 有就直接获取 + JSONObject jsonObject = (JSONObject) webRequest.getAttribute(JSONBODY_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + // 没有就从请求中读取 + if (jsonObject == null) { + String jsonBody = IOUtils.toString(servletRequest.getReader()); + jsonObject = JSON.parseObject(jsonBody); + if (jsonObject != null) { + webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonObject, RequestAttributes.SCOPE_REQUEST); + } + } + return jsonObject; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java new file mode 100644 index 00000000..d2c37fb1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.core.listener; + +import com.orangeforms.common.core.base.service.BaseService; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 应用程序启动后的事件监听对象。主要负责加载Model之间的字典关联和一对一关联所对应的Service结构关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class LoadServiceRelationListener implements ApplicationListener { + + @SuppressWarnings("all") + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + Map serviceMap = + applicationReadyEvent.getApplicationContext().getBeansOfType(BaseService.class); + for (Map.Entry e : serviceMap.entrySet()) { + e.getValue().loadRelationStruct(); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java new file mode 100644 index 00000000..70e09f76 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java @@ -0,0 +1,103 @@ +package com.orangeforms.common.core.object; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +/** + * 业务方法调用结果对象。可以同时返回具体的错误和JSON类型的数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class CallResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final CallResult OK = new CallResult(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误信息描述。 + */ + private String errorMessage = null; + /** + * 在验证同时,仍然需要附加的关联数据对象。 + */ + private JSONObject data; + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static CallResult create(String errorMessage) { + return errorMessage == null ? ok() : error(errorMessage); + } + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @param data 附带的数据对象。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static CallResult create(String errorMessage, JSONObject data) { + return errorMessage == null ? ok(data) : error(errorMessage); + } + + /** + * 创建表示验证成功的对象实例。 + * + * @return 验证成功对象实例。 + */ + public static CallResult ok() { + return OK; + } + + /** + * 创建表示验证成功的对象实例。 + * + * @param data 附带的数据对象。 + * @return 验证成功对象实例。 + */ + public static CallResult ok(JSONObject data) { + CallResult result = new CallResult(); + result.data = data; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @return 验证失败对象实例。 + */ + public static CallResult error(String errorMessage) { + CallResult result = new CallResult(); + result.success = false; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @param data 附带的数据对象。 + * @return 验证失败对象实例。 + */ + public static CallResult error(String errorMessage, T data) { + CallResult result = new CallResult(); + result.success = false; + result.errorMessage = errorMessage; + JSONObject jsonObject = new JSONObject(); + jsonObject.put("errorData", data); + result.data = jsonObject; + return result; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java new file mode 100644 index 00000000..c3422da4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +/** + * 编码字段的编码规则。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class ColumnEncodedRule { + + /** + * 是否显示是计算并回显。 + */ + private Boolean calculateWhenView; + + /** + * 前缀。 + */ + private String prefix; + + /** + * 精确到DAYS/HOURS/MINUTES/SECONDS + */ + private String precisionTo; + + /** + * 中缀。 + */ + private String middle; + + /** + * 流水序号的字符宽度,不足的前面补0。 + */ + private Integer idWidth; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java new file mode 100644 index 00000000..e063b9ab --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +import java.util.List; + +/** + * 常量字典的数据结构。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class ConstDictInfo { + + private List dictData; + + @Data + public static class ConstDictData { + private String type; + private Object id; + private String name; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java new file mode 100644 index 00000000..5806fd02 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.core.object; + +/** + * 哑元对象,主要用于注解中的缺省对象占位符。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DummyClass { + + private static final Object EMPTY_OBJECT = new Object(); + + /** + * 可以忽略的空对象。避免sonarqube的各种警告。 + * + * @return 空对象。 + */ + public static Object emptyObject() { + return EMPTY_OBJECT; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DummyClass() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java new file mode 100644 index 00000000..01b0d437 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.util.BooleanUtil; + +/** + * 线程本地化数据管理的工具类。可根据需求自行添加更多的线程本地化变量及其操作方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class GlobalThreadLocal { + + /** + * 存储数据权限过滤是否启用的线程本地化对象。 + * 目前的过滤条件,包括数据权限和租户过滤。 + */ + private static final ThreadLocal DATA_FILTER_ENABLE = ThreadLocal.withInitial(() -> Boolean.TRUE); + + /** + * 设置数据过滤是否打开。如果打开,当前Servlet线程所执行的SQL操作,均会进行数据过滤。 + * + * @param enable 打开为true,否则false。 + * @return 返回之前的状态,便于恢复。 + */ + public static boolean setDataFilter(boolean enable) { + boolean oldValue = DATA_FILTER_ENABLE.get(); + DATA_FILTER_ENABLE.set(enable); + return oldValue; + } + + /** + * 判断当前Servlet线程所执行的SQL操作,是否进行数据过滤。 + * + * @return true 进行数据权限过滤,否则false。 + */ + public static boolean enabledDataFilter() { + return BooleanUtil.isTrue(DATA_FILTER_ENABLE.get()); + } + + /** + * 清空该存储数据,主动释放线程本地化存储资源。 + */ + public static void clearDataFilter() { + DATA_FILTER_ENABLE.remove(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalThreadLocal() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java new file mode 100644 index 00000000..d33a5908 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; + +/** + * 在线登录用户信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString +@Slf4j +public class LoginUserInfo { + + /** + * 用户Id。 + */ + private Long userId; + /** + * 用户所在部门Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long deptId; + /** + * 租户Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long tenantId; + /** + * 是否为超级管理员。 + */ + private Boolean isAdmin; + /** + * 用户登录名。 + */ + private String loginName; + /** + * 用户显示名称。 + */ + private String showName; + /** + * 标识不同登录的会话Id。 + */ + private String sessionId; + /** + * 登录IP。 + */ + private String loginIp; + /** + * 登录时间。 + */ + private Date loginTime; + /** + * 登录设备类型。 + */ + private String deviceType; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java new file mode 100644 index 00000000..02131aa6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.object; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Mybatis Mapper.xml中所需的分组条件对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@AllArgsConstructor +public class MyGroupCriteria { + + /** + * GROUP BY 从句后面的参数。 + */ + private String groupBy; + /** + * SELECT 从句后面的分组显示字段。 + */ + private String groupSelect; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java new file mode 100644 index 00000000..81fc69b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java @@ -0,0 +1,231 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.config.CoreProperties; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.InvalidClassFieldException; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.InvalidDataModelException; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * 查询分组参数请求对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Data +public class MyGroupParam extends ArrayList { + + private final transient CoreProperties coreProperties = + ApplicationContextHolder.getBean(CoreProperties.class); + + /** + * SQL语句的SELECT LIST中,分组字段的返回字段名称列表。 + */ + private List selectGroupFieldList; + /** + * 分组参数解析后构建的SQL语句中所需的分组数据,如GROUP BY的字段列表和SELECT LIST中的分组字段显示列表。 + */ + private transient MyGroupCriteria groupCriteria; + /** + * 基于分组参数对象中的数据,构建SQL中select list和group by从句可以直接使用的分组对象。 + * + * @param groupParam 分组参数对象。 + * @param modelClazz 查询表对应的主对象的Class。 + * @return SQL中所需的GROUP对象。详见MyGroupCriteria类定义。 + */ + public static MyGroupParam buildGroupBy(MyGroupParam groupParam, Class modelClazz) { + if (groupParam == null) { + return null; + } + if (modelClazz == null) { + throw new IllegalArgumentException("modelClazz Argument can't be NULL"); + } + groupParam.selectGroupFieldList = new LinkedList<>(); + StringBuilder groupByBuilder = new StringBuilder(128); + StringBuilder groupSelectBuilder = new StringBuilder(128); + int i = 0; + for (GroupInfo groupInfo : groupParam) { + GroupBaseData groupBaseData = groupParam.parseGroupBaseData(groupInfo, modelClazz); + if (StrUtil.isBlank(groupBaseData.tableName)) { + throw new InvalidDataModelException(groupBaseData.modelName); + } + if (StrUtil.isBlank(groupBaseData.columnName)) { + throw new InvalidDataFieldException(groupBaseData.modelName, groupBaseData.fieldName); + } + groupParam.processGroupInfo(groupInfo, groupBaseData, groupByBuilder, groupSelectBuilder); + String aliasName = StrUtil.isBlank(groupInfo.aliasName) ? groupInfo.fieldName : groupInfo.aliasName; + // selectGroupFieldList中的元素,目前只是被export操作使用。会根据集合中的元素名称匹配导出表头。 + groupParam.selectGroupFieldList.add(aliasName); + if (++i < groupParam.size()) { + groupByBuilder.append(", "); + groupSelectBuilder.append(", "); + } + } + groupParam.groupCriteria = new MyGroupCriteria(groupByBuilder.toString(), groupSelectBuilder.toString()); + return groupParam; + } + + private GroupBaseData parseGroupBaseData(GroupInfo groupInfo, Class modelClazz) { + GroupBaseData baseData = new GroupBaseData(); + if (StrUtil.isBlank(groupInfo.fieldName)) { + throw new IllegalArgumentException("GroupInfo.fieldName can't be EMPTY"); + } + String[] stringArray = StrUtil.splitToArray(groupInfo.fieldName, '.'); + if (stringArray.length == 1) { + baseData.modelName = modelClazz.getSimpleName(); + baseData.fieldName = groupInfo.fieldName; + baseData.tableName = MyModelUtil.mapToTableName(modelClazz); + baseData.columnName = MyModelUtil.mapToColumnName(groupInfo.fieldName, modelClazz); + } else { + Field field = ReflectUtil.getField(modelClazz, stringArray[0]); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]); + } + Class fieldClazz = field.getType(); + baseData.modelName = fieldClazz.getSimpleName(); + baseData.fieldName = stringArray[1]; + baseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + baseData.columnName = MyModelUtil.mapToColumnName(baseData.fieldName, fieldClazz); + } + return baseData; + } + + private void processGroupInfo( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String tableName = baseData.tableName; + String columnName = baseData.columnName; + if (StrUtil.isBlank(groupInfo.dateAggregateBy)) { + groupBy.append(tableName).append(".").append(columnName); + groupSelect.append(tableName).append(".").append(columnName); + if (StrUtil.isNotBlank(groupInfo.aliasName)) { + groupSelect.append(" ").append(groupInfo.aliasName); + } + return; + } + if (coreProperties.isMySql() || coreProperties.isDm()) { + this.processMySqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else if (coreProperties.isPostgresql() || coreProperties.isOpenGauss()) { + this.processPostgreSqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else if (coreProperties.isOracle() || coreProperties.isKingbase()) { + this.processOracleGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else { + throw new UnsupportedOperationException("Unsupport Database Type."); + } + if (StrUtil.isNotBlank(groupInfo.aliasName)) { + groupSelect.append(" ").append(groupInfo.aliasName); + } else { + groupSelect.append(" ").append(columnName); + } + } + + private void processMySqlGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + groupBy.append("DATE_FORMAT(") + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append("DATE_FORMAT(") + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-%m-%d')"); + groupSelect.append(", '%Y-%m-%d')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-%m-01')"); + groupSelect.append(", '%Y-%m-01')"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-01-01')"); + groupSelect.append(", '%Y-01-01')"); + } else { + throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list."); + } + } + + private void processPostgreSqlGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String toCharFunc = "TO_CHAR("; + String dateFormat = ", 'YYYY-MM-dd')"; + groupBy.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(dateFormat); + groupSelect.append(dateFormat); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-01-01')"); + groupSelect.append(", 'YYYY-01-01')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-MM-01')"); + groupSelect.append(", 'YYYY-MM-01')"); + } else { + throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list."); + } + } + + private void processOracleGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String toCharFunc = "TO_CHAR("; + String dateFormat = ", 'YYYY-MM-dd')"; + groupBy.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(dateFormat); + groupSelect.append(dateFormat); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-MM') || '-01'"); + groupSelect.append(", 'YYYY-MM') || '-01'"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY') || '-01-01'"); + groupSelect.append(", 'YYYY') || '-01-01'"); + } else { + throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list."); + } + } + + /** + * 分组信息对象。 + */ + @Data + public static class GroupInfo { + /** + * Java对象的字段名。目前主要包含三种格式: + * 1. 简单的属性名称,如userId,将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。 + * 映射结果或为 my_main_table.user_id + * 2. 一对一关联表属性,如user.userId,这里将先获取user属性的对象类型并映射到对应的表名,后面的userId为 + * user所在实体的属性。映射结果或为:my_sys_user.user_id + */ + private String fieldName; + /** + * SQL语句的Select List中,分组字段的别名。如果别名为NULL,直接取fieldName。 + */ + private String aliasName; + /** + * 如果该值不为NULL,则会对分组字段进行DATE_FORMAT函数的计算,并根据具体的值,将日期数据截取到指定的位。 + * day: 表示按照天聚合,将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d') + * month: 表示按照月聚合,将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01') + * year: 表示按照年聚合,将会截取到年。DATE_FORMAT(columnName, '%Y-01-01') + */ + private String dateAggregateBy; + } + + private static class GroupBaseData { + private String modelName; + private String fieldName; + private String tableName; + private String columnName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java new file mode 100644 index 00000000..4ae6fb3e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java @@ -0,0 +1,303 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.util.ReflectUtil; +import com.mybatisflex.annotation.Id; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.InvalidClassFieldException; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.InvalidDataModelException; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Controller参数中的排序请求对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Data +public class MyOrderParam extends ArrayList { + + private static final String DICT_MAP = "DictMap."; + private static final Map, MyOrderParam> DEFAULT_ORDER_PARAM_MAP = new ConcurrentHashMap<>(); + + /** + * 基于排序对象中的JSON数据,构建SQL中order by从句可以直接使用的排序字符串。 + * 注意:如果orderParam为NULL,则会通过modelClazz对象推演出主键字典名,并按照主键倒排的方式生成默认的排序对象。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @return SQL中order by从句可以直接使用的排序字符串。 + */ + public static String buildOrderBy(MyOrderParam orderParam, Class modelClazz) { + return buildOrderBy(orderParam, modelClazz, true); + } + + /** + * 基于排序对象中的JSON数据,构建SQL中order by从句可以直接使用的排序字符串。 + * 注意:如果orderParam为NULL,则会通过modelClazz对象推演出主键字典名,并按照主键倒排的方式生成默认的排序对象。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param addDefaultIfNull 如果为true,当orderParam参数为NULL是,则自动添加基于主键倒排序的索引。 + * @return SQL中order by从句可以直接使用的排序字符串。 + */ + public static String buildOrderBy(MyOrderParam orderParam, Class modelClazz, boolean addDefaultIfNull) { + if (orderParam == null) { + if (!addDefaultIfNull) { + return null; + } + orderParam = getAndSetDefaultOrderParam(modelClazz); + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.buildOrderBy can't be NULL"); + } + int i = 0; + StringBuilder orderBy = new StringBuilder(128); + for (OrderInfo orderInfo : orderParam) { + if (StringUtils.isBlank(orderInfo.getFieldName())) { + continue; + } + OrderBaseData orderBaseData = parseOrderBaseData(orderInfo, modelClazz); + if (StringUtils.isBlank(orderBaseData.tableName)) { + throw new InvalidDataModelException(orderBaseData.modelName); + } + if (StringUtils.isBlank(orderBaseData.columnName)) { + throw new InvalidDataFieldException(orderBaseData.modelName, orderBaseData.fieldName); + } + processOrderInfo(orderInfo, orderBaseData, orderBy); + if (++i < orderParam.size()) { + orderBy.append(", "); + } + } + return orderBy.toString(); + } + + private static MyOrderParam getAndSetDefaultOrderParam(Class modelClazz) { + MyOrderParam orderParam = DEFAULT_ORDER_PARAM_MAP.get(modelClazz); + if (orderParam != null) { + return orderParam; + } + orderParam = new MyOrderParam(); + DEFAULT_ORDER_PARAM_MAP.put(modelClazz, orderParam); + Field[] fields = ReflectUtil.getFields(modelClazz); + for (Field field : fields) { + if (field.getAnnotation(Id.class) != null) { + orderParam.add(new OrderInfo(field.getName(), false, null)); + break; + } + } + return orderParam; + } + + private static void processOrderInfo( + OrderInfo orderInfo, OrderBaseData orderBaseData, StringBuilder orderByBuilder) { + if (StringUtils.isNotBlank(orderInfo.dateAggregateBy)) { + orderByBuilder.append("DATE_FORMAT(") + .append(orderBaseData.tableName).append(".").append(orderBaseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-%m-%d')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-%m-01')"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-01-01')"); + } else { + throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list."); + } + } else { + orderByBuilder.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName); + } + if (orderInfo.asc != null && !orderInfo.asc) { + orderByBuilder.append(" DESC"); + } + } + + private static OrderBaseData parseOrderBaseData(OrderInfo orderInfo, Class modelClazz) { + OrderBaseData orderBaseData = new OrderBaseData(); + orderBaseData.fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + String[] stringArray = StringUtils.split(orderBaseData.fieldName, '.'); + if (stringArray.length == 1) { + orderBaseData.modelName = modelClazz.getSimpleName(); + orderBaseData.tableName = MyModelUtil.mapToTableName(modelClazz); + orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, modelClazz); + } else { + Field field = ReflectUtil.getField(modelClazz, stringArray[0]); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]); + } + Class fieldClazz = field.getType(); + orderBaseData.modelName = fieldClazz.getSimpleName(); + orderBaseData.fieldName = stringArray[1]; + orderBaseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, fieldClazz); + } + return orderBaseData; + } + + /** + * 在排序列表中,可能存在基于指定表字段的排序,该函数将获取指定表的所有排序字段。 + * 返回的字符串,可直接用于SQL中的ORDER BY从句。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param relationModelName 与关联表对应的Model的名称,如my_course_paper表应对的Java对象CoursePaper。 + * 如果该值为null或空字符串,则获取所有主表的排序字段。 + * @return 返回的是表字段,而非Java对象的属性,多个字段之间逗号分隔。 + */ + public static String getOrderClauseByModelName( + MyOrderParam orderParam, Class modelClazz, String relationModelName) { + if (orderParam == null) { + return null; + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.getOrderClauseByModelName can't be NULL"); + } + List fieldNameList = new LinkedList<>(); + String prefix = null; + if (StringUtils.isNotBlank(relationModelName)) { + prefix = relationModelName + "."; + } + for (OrderInfo orderInfo : orderParam) { + OrderBaseData baseData = parseOrderBaseData(orderInfo, modelClazz, prefix, relationModelName); + if (baseData != null) { + fieldNameList.add(makeOrderBy(baseData, orderInfo.asc)); + } + } + return StringUtils.join(fieldNameList, ", "); + } + + private static OrderBaseData parseOrderBaseData( + OrderInfo orderInfo, Class modelClazz, String prefix, String relationModelName) { + OrderBaseData baseData = null; + String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + if (prefix != null) { + if (fieldName.startsWith(prefix)) { + baseData = new OrderBaseData(); + Field field = ReflectUtil.getField(modelClazz, relationModelName); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), relationModelName); + } + Class fieldClazz = field.getType(); + baseData.modelName = fieldClazz.getSimpleName(); + baseData.fieldName = StringUtils.removeStart(fieldName, prefix); + baseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + baseData.columnName = MyModelUtil.mapToColumnName(fieldName, fieldClazz); + } + } else { + String dotLimitor = "."; + if (!fieldName.contains(dotLimitor)) { + baseData = new OrderBaseData(); + baseData.modelName = modelClazz.getSimpleName(); + baseData.tableName = MyModelUtil.mapToTableName(modelClazz); + baseData.columnName = MyModelUtil.mapToColumnName(fieldName, modelClazz); + } + } + return baseData; + } + + private static String makeOrderBy(OrderBaseData baseData, Boolean asc) { + if (StringUtils.isBlank(baseData.tableName)) { + throw new InvalidDataModelException(baseData.modelName); + } + if (StringUtils.isBlank(baseData.columnName)) { + throw new InvalidDataFieldException(baseData.modelName, baseData.fieldName); + } + StringBuilder orderBy = new StringBuilder(128); + orderBy.append(baseData.tableName).append(".").append(baseData.columnName); + if (asc != null && !asc) { + orderBy.append(" DESC"); + } + return orderBy.toString(); + } + + /** + * 在排序列表中,可能存在基于指定表字段的排序,该函数将删除指定表的所有排序字段。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param relationModelName 与关联表对应的Model的名称,如my_course_paper表应对的Java对象CoursePaper。 + * 如果该值为null或空字符串,则获取所有主表的排序字段。 + */ + public static void removeOrderClauseByModelName( + MyOrderParam orderParam, Class modelClazz, String relationModelName) { + if (orderParam == null) { + return; + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.removeOrderClauseByModelName can't be NULL"); + } + List fieldIndexList = new LinkedList<>(); + String prefix = null; + if (StringUtils.isNotBlank(relationModelName)) { + prefix = relationModelName + "."; + } + int i = 0; + for (OrderInfo orderInfo : orderParam) { + String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + if (prefix != null) { + if (fieldName.startsWith(prefix)) { + fieldIndexList.add(i); + } + } else { + if (!fieldName.contains(".")) { + fieldIndexList.add(i); + } + } + ++i; + } + for (int index : fieldIndexList) { + orderParam.remove(index); + } + } + + /** + * 排序信息对象。 + */ + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class OrderInfo { + /** + * Java对象的字段名。如果fieldName为空,则忽略跳过。目前主要包含三种格式: + * 1. 简单的属性名称,如userId,将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。 + * 映射结果或为 my_main_table.user_id + * 2. 字典属性名称,如userIdDictMap.id,由于仅仅支持字典中Id数据的排序,所以直接截取DictMap之前的字符串userId作为排序属性。 + * 表名为当前ModelClazz所对应的表名。映射结果或为 my_main_table.user_id + * 3. 一对一关联表属性,如user.userId,这里将先获取user属性的对象类型并映射到对应的表名,后面的userId为 + * user所在实体的属性。映射结果或为:my_sys_user.user_id + */ + private String fieldName; + /** + * 排序方向。true为升序,否则降序。 + */ + private Boolean asc = true; + /** + * 如果该值不为NULL,则会对日期型排序字段进行DATE_FORMAT函数的计算,并根据具体的值,将日期数据截取到指定的位。 + * day: 表示按照天聚合,将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d') + * month: 表示按照月聚合,将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01') + * year: 表示按照年聚合,将会截取到年。DATE_FORMAT(columnName, '%Y-01-01') + */ + private String dateAggregateBy; + } + + private static class OrderBaseData { + private String modelName; + private String fieldName; + private String tableName; + private String columnName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java new file mode 100644 index 00000000..57bb1c8f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.core.object; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.LinkedList; +import java.util.List; + +/** + * 分页数据的应答返回对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MyPageData { + /** + * 数据列表。 + */ + private List dataList; + /** + * 数据总数量。 + */ + private Long totalCount; + + /** + * 为了保持前端的数据格式兼容性,在没有数据的时候,需要返回空分页对象。 + * @return 空分页对象。 + */ + public static MyPageData emptyPageData() { + return new MyPageData<>(new LinkedList<>(), 0L); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java new file mode 100644 index 00000000..cd4ddc41 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.core.object; + +import lombok.Getter; + +/** + * Controller参数中的分页请求对象 + * + * @author Jerry + * @date 2024-07-02 + */ +@Getter +public class MyPageParam { + + public static final int DEFAULT_PAGE_NUM = 1; + public static final int DEFAULT_PAGE_SIZE = 10; + public static final int DEFAULT_MAX_SIZE = 2000; + + /** + * 分页号码,从1开始计数。 + */ + private Integer pageNum; + + /** + * 每页大小。 + */ + private Integer pageSize; + + /** + * 是否统计totalCount + */ + private Boolean count = true; + + /** + * 设置当前分页页号。 + * + * @param pageNum 页号,如果传入非法值,则使用缺省值。 + */ + public void setPageNum(Integer pageNum) { + if (pageNum == null) { + return; + } + if (pageNum <= 0) { + pageNum = DEFAULT_PAGE_NUM; + } + this.pageNum = pageNum; + } + + /** + * 设置分页的大小。 + * + * @param pageSize 分页大小,如果传入非法值,则使用缺省值。 + */ + public void setPageSize(Integer pageSize) { + if (pageSize == null) { + return; + } + if (pageSize <= 0) { + pageSize = DEFAULT_PAGE_SIZE; + } + if (pageSize > DEFAULT_MAX_SIZE) { + pageSize = DEFAULT_MAX_SIZE; + } + this.pageSize = pageSize; + } + + public void setCount(Boolean count) { + this.count = count; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java new file mode 100644 index 00000000..6a5a60d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java @@ -0,0 +1,32 @@ +package com.orangeforms.common.core.object; + +import com.alibaba.fastjson.JSONArray; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 打印信息对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +public class MyPrintInfo { + + /** + * 打印模板Id。 + */ + private Long printId; + /** + * 打印参数列表。对应于common-report模块的ReportPrintParam对象。 + */ + private List printParams; + + public MyPrintInfo(Long printId, List printParams) { + this.printId = printId; + this.printParams = printParams; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java new file mode 100644 index 00000000..26f23c15 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java @@ -0,0 +1,122 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 实体对象数据组装参数构建器。 + * BaseService中的实体对象数据组装函数,会根据该参数对象进行数据组装。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Builder +public class MyRelationParam { + + /** + * 是否组装字典关联的标记。 + * 组装RelationDict和RelationConstDict注解标记的字段。 + */ + private boolean buildDict; + + /** + * 是否组装一对一关联的标记。 + * 组装RelationOneToOne注解标记的字段。 + */ + private boolean buildOneToOne; + + /** + * 是否组装一对多关联的标记。 + * 组装RelationOneToMany注解标记的字段。 + */ + private boolean buildOneToMany; + + /** + * 在组装一对一关联的同时,是否继续关联从表中的字典。 + * 从表中RelationDict和RelationConstDict注解标记的字段。 + * 该字段为true时,无需设置buildOneToOne了。 + */ + private boolean buildOneToOneWithDict; + + /** + * 是否组装主表对多对多中间表关联的标记。 + * 组装RelationManyToMany注解标记的字段。 + */ + private boolean buildRelationManyToMany; + + /** + * 是否组装聚合计算关联的标记。 + * 组装RelationOneToManyAggregation和RelationManyToManyAggregation注解标记的字段。 + */ + private boolean buildRelationAggregation; + + /** + * 关联表中,需要忽略的脱敏字段名。key是关联表实体对象名,如SysUser,value是对象字段名的集合,如userId。 + */ + @Getter + private Map> ignoreMaskFieldMap; + + /** + * 关联表中需要忽略的脱敏字段结合。 + * @param ignoreRelationMaskFieldSet 数据项格式为"实体对象名.对象属性名",如 sysUser.userId。 + */ + public void setIgnoreMaskFieldSet(Set ignoreRelationMaskFieldSet) { + if (CollUtil.isEmpty(ignoreRelationMaskFieldSet)) { + return; + } + ignoreMaskFieldMap = MapUtil.newHashMap(); + for (String ignoreField : ignoreRelationMaskFieldSet) { + String[] fullFieldName = StrUtil.splitToArray(ignoreField, "."); + Set ignoreMaskFieldSet = + ignoreMaskFieldMap.computeIfAbsent(fullFieldName[0], k -> new HashSet<>()); + ignoreMaskFieldSet.add(fullFieldName[1]); + } + } + + /** + * 便捷方法,返回仅做字典关联的参数对象。 + * + * @return 返回仅做字典关联的参数对象。 + */ + public static MyRelationParam dictOnly() { + return MyRelationParam.builder().buildDict(true).build(); + } + + /** + * 便捷方法,返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。 + * NOTE: 对于一对多和多对多,这种从表数据是列表结果的关联,均不返回。 + * + * @return 返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。 + */ + public static MyRelationParam normal() { + return MyRelationParam.builder() + .buildDict(true) + .buildOneToOneWithDict(true) + .buildRelationAggregation(true) + .build(); + } + + /** + * 便捷方法,返回全部关联的参数对象。 + * + * @return 返回全部关联的参数对象。 + */ + public static MyRelationParam full() { + return MyRelationParam.builder() + .buildDict(true) + .buildOneToOneWithDict(true) + .buildRelationAggregation(true) + .buildRelationManyToMany(true) + .buildOneToMany(true) + .build(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java new file mode 100644 index 00000000..d225446c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java @@ -0,0 +1,376 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.alibaba.fastjson.annotation.JSONField; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.InvalidDataModelException; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; + +/** + * Where中的条件语句。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Data +@NoArgsConstructor +public class MyWhereCriteria { + + /** + * 等于 + */ + public static final int OPERATOR_EQUAL = 0; + + /** + * 不等于 + */ + public static final int OPERATOR_NOT_EQUAL = 1; + + /** + * 大于等于 + */ + public static final int OPERATOR_GE = 2; + + /** + * 大于 + */ + public static final int OPERATOR_GT = 3; + + /** + * 小于等于 + */ + public static final int OPERATOR_LE = 4; + + /** + * 小于 + */ + public static final int OPERATOR_LT = 5; + + /** + * LIKE + */ + public static final int OPERATOR_LIKE = 6; + + /** + * NOT NULL + */ + public static final int OPERATOR_NOT_NULL = 7; + + /** + * IS NULL + */ + public static final int OPERATOR_IS_NULL = 8; + + /** + * IN + */ + public static final int OPERATOR_IN = 9; + + /** + * 参与过滤的实体对象的Class。 + */ + @JSONField(serialize = false) + private Class modelClazz; + + /** + * 数据库表名。 + */ + private String tableName; + + /** + * Java属性名称。 + */ + private String fieldName; + + /** + * 数据表字段名。 + */ + private String columnName; + + /** + * 数据表字段类型。 + */ + private Integer columnType; + + /** + * 操作符类型,取值范围见上面的常量值。 + */ + private Integer operatorType; + + /** + * 条件数据值。 + */ + private Object value; + + public MyWhereCriteria(Class modelClazz, String fieldName, Integer operatorType, Object value) { + this.modelClazz = modelClazz; + this.fieldName = fieldName; + this.operatorType = operatorType; + this.value = value; + } + + /** + * 设置条件值。 + * + * @param fieldName 条件所属的实体对象的字段名。 + * @param operatorType 条件操作符。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult setCriteria(String fieldName, Integer operatorType, Object value) { + this.operatorType = operatorType; + this.fieldName = fieldName; + this.value = value; + return doVerify(); + } + + /** + * 设置条件值。 + * + * @param modelClazz 数据表对应实体对象的Class. + * @param fieldName 条件所属的实体对象的字段名。 + * @param operatorType 条件操作符。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult setCriteria(Class modelClazz, String fieldName, Integer operatorType, Object value) { + this.modelClazz = modelClazz; + this.operatorType = operatorType; + this.fieldName = fieldName; + this.value = value; + return doVerify(); + } + + /** + * 设置条件值,通过该构造方法设置时,通常是直接将表名、字段名、字段类型等赋值,无需在通过modelClazz进行推演。 + * + * @param tableName 数据表名。 + * @param columnName 数据字段名。 + * @param columnType 数据字段类型。 + * @param operatorType 操作类型。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + */ + public void setCriteria( + String tableName, String columnName, String columnType, Integer operatorType, Object value) { + this.tableName = tableName; + this.columnName = columnName; + this.columnType = MyModelUtil.NUMERIC_FIELD_TYPE; + if (String.class.getSimpleName().equals(columnType)) { + this.columnType = MyModelUtil.STRING_FIELD_TYPE; + } else if (Date.class.getSimpleName().equals(columnType)) { + this.columnType = MyModelUtil.DATE_FIELD_TYPE; + } + this.operatorType = operatorType; + this.value = value; + } + + /** + * 在执行该函数之前,该对象的所有数据均已经赋值完毕。 + * 该函数主要验证操作符字段和条件值字段对应关系的合法性。 + * + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult doVerify() { + if (fieldName == null) { + return CallResult.error("过滤字段名称 [fieldName] 不能为空!"); + } + if (modelClazz != null && ReflectUtil.getField(modelClazz, fieldName) == null) { + return CallResult.error( + "过滤字段 [" + fieldName + "] 在实体对象 [" + modelClazz.getSimpleName() + "] 中并不存在!"); + } + if (!checkOperatorType()) { + return CallResult.error("无效的操作符类型 [" + operatorType + "]!"); + } + // 其他操作符必须包含value值 + if (operatorType != OPERATOR_IS_NULL && operatorType != OPERATOR_NOT_NULL && value == null) { + String operatorString = this.getOperatorString(); + return CallResult.error("操作符 [" + operatorString + "] 的条件值不能为空!"); + } + if (this.operatorType == OPERATOR_IN) { + if (!(value instanceof Collection)) { + return CallResult.error("操作符 [IN] 的条件值必须为集合对象!"); + } + if (CollUtil.isEmpty((Collection) value)) { + return CallResult.error("操作符 [IN] 的条件值不能为空!"); + } + } + return CallResult.ok(); + } + + /** + * 判断操作符类型是否合法。 + * + * @return 合法返回true,否则false。 + */ + public boolean checkOperatorType() { + return operatorType != null + && (operatorType >= OPERATOR_EQUAL && operatorType <= OPERATOR_IN); + } + + /** + * 获取操作符的字符串形式。 + * + * @return 操作符的字符串。 + */ + public String getOperatorString() { + switch (operatorType) { + case OPERATOR_EQUAL: + return " = "; + case OPERATOR_NOT_EQUAL: + return " != "; + case OPERATOR_GE: + return " >= "; + case OPERATOR_GT: + return " > "; + case OPERATOR_LE: + return " <= "; + case OPERATOR_LT: + return " < "; + case OPERATOR_LIKE: + return " LIKE "; + case OPERATOR_NOT_NULL: + return " IS NOT NULL "; + case OPERATOR_IS_NULL: + return " IS NULL "; + case OPERATOR_IN: + return " IN "; + default: + return null; + } + } + + /** + * 获取组装后的SQL Where从句,如 table_name.column_name = 'value'。 + * 与查询数据表对应的实体对象Class为当前对象的modelClazz字段。 + * + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public String makeCriteriaString() { + return makeCriteriaString(this.modelClazz); + } + + /** + * 获取组装后的SQL Where从句,如 table_name.column_name = 'value'。 + * + * @param modelClazz 与查询数据表对应的实体对象的Class。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @exception InvalidDataModelException 参数modelClazz没有对应的table,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public String makeCriteriaString(Class modelClazz) { + String localTableName; + String localColumnName; + Integer localColumnType; + if (modelClazz != null) { + Tuple2 fieldInfo = MyModelUtil.mapToColumnInfo(fieldName, modelClazz); + if (fieldInfo == null) { + throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName); + } + localColumnName = fieldInfo.getFirst(); + localColumnType = fieldInfo.getSecond(); + localTableName = MyModelUtil.mapToTableName(modelClazz); + if (localTableName == null) { + throw new InvalidDataModelException(modelClazz.getSimpleName()); + } + } else { + localTableName = this.tableName; + localColumnName = this.columnName; + localColumnType = this.columnType; + } + return this.buildClauseString(localTableName, localColumnName, localColumnType); + } + + /** + * 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。 + * + * @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public static String makeCriteriaString(List criteriaList) { + return makeCriteriaString(criteriaList, null); + } + + /** + * 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。 + * + * @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。 + * @param modelClazz 与数据表对应的实体对象的Class。 + * 如果不为NULL实体对象Class使用该值,否则使用每个MyWhereCriteria自身的modelClazz。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public static String makeCriteriaString(List criteriaList, Class modelClazz) { + if (CollUtil.isEmpty(criteriaList)) { + return null; + } + StringBuilder sb = new StringBuilder(256); + int i = 0; + for (MyWhereCriteria whereCriteria : criteriaList) { + Class clazz = modelClazz; + if (clazz == null) { + clazz = whereCriteria.modelClazz; + } + if (i++ != 0) { + sb.append(" AND "); + } + String criteriaString = whereCriteria.makeCriteriaString(clazz); + sb.append(criteriaString); + } + return sb.length() == 0 ? null : sb.toString(); + } + + private String buildClauseString(String tableName, String columnName, Integer columnType) { + StringBuilder sb = new StringBuilder(64); + sb.append(tableName).append(".").append(columnName).append(getOperatorString()); + if (operatorType == OPERATOR_IN) { + Collection filterValues = (Collection) value; + sb.append("("); + int i = 0; + for (Object filterValue : filterValues) { + this.doSqlInjectVerify(filterValue.toString()); + if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) { + sb.append(filterValue); + } else { + sb.append("'").append(filterValue).append("'"); + } + if (i++ != filterValues.size() - 1) { + sb.append(", "); + } + } + sb.append(")"); + return sb.toString(); + } + if (value == null) { + return sb.toString(); + } + this.doSqlInjectVerify(value.toString()); + if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) { + sb.append(value); + } else { + sb.append("'").append(value).append("'"); + } + return sb.toString(); + } + + private void doSqlInjectVerify(String v) { + Matcher matcher = ApplicationConstant.SQL_INJECT_PATTERN.matcher(v); + if (matcher.find()) { + String msg = String.format( + "The filterValue [%s] has SQL Inject Words", v); + throw new MyRuntimeException(msg); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java new file mode 100644 index 00000000..26e2eee5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java @@ -0,0 +1,295 @@ +package com.orangeforms.common.core.object; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * 接口返回对象 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Data +public class ResponseResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final ResponseResult OK = new ResponseResult<>(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误码。 + */ + private String errorCode = "NO-ERROR"; + /** + * 错误信息描述。 + */ + private String errorMessage = "NO-MESSAGE"; + /** + * 实际数据。 + */ + private T data = null; + /** + * HTTP状态码,通常用于内部调用的方法传递,不推荐返回给前端。 + */ + @JSONField(serialize = false) + private int httpStatus = 200; + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param errorCodeEnum 错误码枚举。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum) { + return create(errorCodeEnum, errorCodeEnum.getErrorMessage()); + } + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 如果该参数为null,错误信息取自errorCodeEnum参数内置的errorMessage,否则使用当前参数。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum, String errorMessage) { + errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage(); + return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success() : error(errorCodeEnum.name(), errorMessage); + } + + /** + * 根据参数errorCode是否为空,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。 + * + * @param errorCode 自定义的错误码。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(String errorCode, String errorMessage) { + return errorCode == null ? success() : error(errorCode, errorMessage); + } + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 如果该参数为null,错误信息取自errorCodeEnum参数内置的errorMessage,否则使用当前参数。 + * @param data 如果错误枚举值为NO_ERROR,则返回该数据。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum, String errorMessage, T data) { + errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage(); + return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success(data) : error(errorCodeEnum.name(), errorMessage); + } + + /** + * 创建成功对象。 + * 如果需要绑定返回数据,可以在实例化后调用setDataObject方法。 + * + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult success() { + return OK; + } + + /** + * 创建带有返回数据的成功对象。 + * + * @param data 返回的数据对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult success(T data) { + ResponseResult resp = new ResponseResult<>(); + resp.data = data; + return resp; + } + + /** + * 创建带有返回数据的成功对象。 + * + * @param data 返回的数据对象。 + * @param clazz 目标数据类型。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult success(R data, Class clazz) { + ResponseResult resp = new ResponseResult<>(); + resp.data = MyModelUtil.copyTo(data, clazz); + return resp; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param errorCodeEnum 错误码枚举。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(ErrorCodeEnum errorCodeEnum) { + return error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage()); + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param httpStatus http状态值。 + * @param errorCodeEnum 错误码枚举。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(int httpStatus, ErrorCodeEnum errorCodeEnum) { + ResponseResult r = error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage()); + r.setHttpStatus(httpStatus); + return r; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(ErrorCodeEnum errorCodeEnum, String errorMessage) { + return error(errorCodeEnum.name(), errorMessage); + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param httpStatus http状态值。 + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(int httpStatus, ErrorCodeEnum errorCodeEnum, String errorMessage) { + ResponseResult r = error(errorCodeEnum.name(), errorMessage); + r.setHttpStatus(httpStatus); + return r; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。 + * + * @param errorCode 自定义的错误码。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(String errorCode, String errorMessage) { + return new ResponseResult<>(errorCode, errorMessage); + } + + /** + * 根据参数中出错的ResponseResult,创建新的错误应答对象。 + * + * @param errorCause 导致错误原因的应答对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult errorFrom(ResponseResult errorCause) { + return error(errorCause.errorCode, errorCause.getErrorMessage()); + } + + /** + * 根据参数中出错的CallResult,创建新的错误应答对象。 + * + * @param errorCause 导致错误原因的应答对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult errorFrom(CallResult errorCause) { + return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorCause.getErrorMessage()); + } + + /** + * 根据参数中CallResult,创建新的应答对象。 + * + * @param result CallResult对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult from(CallResult result) { + if (result.isSuccess()) { + return success(); + } + return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + + /** + * 是否成功。 + * + * @return true成功,否则false。 + */ + public boolean isSuccess() { + return success; + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。 + * + * @param httpStatus http状态码。 + * @param responseResult 应答内容。 + * @param 数据对象类型。 + * @throws IOException 异常错误。 + */ + public static void output(int httpStatus, ResponseResult responseResult) throws IOException { + if (httpStatus != HttpServletResponse.SC_OK) { + log.error(JSON.toJSONString(responseResult)); + } else { + log.info(JSON.toJSONString(responseResult)); + } + HttpServletResponse response = ContextUtil.getHttpResponse(); + PrintWriter out = response.getWriter(); + response.setContentType("application/json; charset=utf-8"); + response.setStatus(httpStatus); + if (responseResult != null) { + out.print(JSON.toJSONString(responseResult)); + } + out.flush(); + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。 + * + * @param httpStatus http状态码。 + * @throws IOException 异常错误。 + */ + public static void output(int httpStatus) throws IOException { + output(httpStatus, null); + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。Http状态码为200。 + * + * @param responseResult 应答内容。 + * @param 数据对象类型。 + * @throws IOException 异常错误。 + */ + public static void output(ResponseResult responseResult) throws IOException { + output(HttpServletResponse.SC_OK, responseResult); + } + + private ResponseResult() { + } + + private ResponseResult(String errorCode, String errorMessage) { + this.success = false; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java new file mode 100644 index 00000000..71c9d594 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +/** + * 数据表模型基础信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class TableModelInfo { + + /** + * 数据表名。 + */ + private String tableName; + + /** + * 实体对象名。 + */ + private String modelName; + + /** + * 主键的表字段名。 + */ + private String keyColumnName; + + /** + * 主键在实体对象中的属性名。 + */ + private String keyFieldName; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java new file mode 100644 index 00000000..79f3c1f9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java @@ -0,0 +1,134 @@ +package com.orangeforms.common.core.object; + +import com.orangeforms.common.core.util.ContextUtil; +import lombok.Data; +import lombok.ToString; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; + +/** + * 基于Jwt,用于前后端传递的令牌对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString +public class TokenData { + + /** + * 在HTTP Request对象中的属性键。 + */ + public static final String REQUEST_ATTRIBUTE_NAME = "tokenData"; + /** + * 是否为百分号编码后的TokenData数据。 + */ + public static final String REQUEST_ENCODED_TOKEN = "encodedTokenData"; + /** + * 用户Id。 + */ + private Long userId; + /** + * 用户所属角色。多个角色之间逗号分隔。 + */ + private String roleIds; + /** + * 用户所在部门Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long deptId; + /** + * 用户所属岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。 + */ + private String postIds; + /** + * 用户的部门岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。 + */ + private String deptPostIds; + /** + * 租户Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long tenantId; + /** + * 是否为超级管理员。 + */ + private Boolean isAdmin; + /** + * 用户登录名。 + */ + private String loginName; + /** + * 用户显示名称。 + */ + private String showName; + /** + * 所在部门名。 + */ + private String deptName; + /** + * 设备类型。参考AppDeviceType。 + */ + private String deviceType; + /** + * 标识不同登录的会话Id。 + */ + private String sessionId; + /** + * 目前仅用于SaToken权限框架。 + * 主要用于辅助管理在线用户数据,SaToken默认的功能对于租户Id和登录用户的查询,没有提供方便的支持,或是效率较低。 + */ + private String mySessionId; + /** + * 访问uaa的授权token。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private String uaaAccessToken; + /** + * 数据库路由键(仅当水平分库时使用)。 + */ + private Integer datasourceType; + /** + * 登录IP。 + */ + private String loginIp; + /** + * 登录时间。 + */ + private Date loginTime; + /** + * 登录头像地址。 + */ + private String headImageUrl; + /** + * 原始的请求Token。 + */ + private String token; + /** + * 应用编码。空值表示非第三方应用。 + */ + private String appCode; + + /** + * 将令牌对象添加到Http请求对象。 + * + * @param tokenData 令牌对象。 + */ + public static void addToRequest(TokenData tokenData) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request != null) { + request.setAttribute(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData); + } + } + + /** + * 从Http Request对象中获取令牌对象。 + * + * @return 令牌对象。 + */ + public static TokenData takeFromRequest() { + HttpServletRequest request = ContextUtil.getHttpRequest(); + return request == null ? null : (TokenData) request.getAttribute(REQUEST_ATTRIBUTE_NAME); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java new file mode 100644 index 00000000..19799a3e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java @@ -0,0 +1,50 @@ +package com.orangeforms.common.core.object; + +/** + * 二元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class Tuple2 { + + /** + * 第一个变量。 + */ + private final T1 first; + /** + * 第二个变量。 + */ + private final T2 second; + + /** + * 构造函数。 + * + * @param first 第一个变量。 + * @param second 第二个变量。 + */ + public Tuple2(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + /** + * 获取第一个变量。 + * + * @return 返回第一个变量。 + */ + public T1 getFirst() { + return first; + } + + /** + * 获取第二个变量。 + * + * @return 返回第二个变量。 + */ + public T2 getSecond() { + return second; + } + +} + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java new file mode 100644 index 00000000..bc6e4b7e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java @@ -0,0 +1,65 @@ +package com.orangeforms.common.core.object; + +/** + * 三元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class Tuple3 { + + /** + * 第一个变量。 + */ + private final T1 first; + /** + * 第二个变量。 + */ + private final T2 second; + + /** + * 第三个变量。 + */ + private final T3 third; + + /** + * 构造函数。 + * + * @param first 第一个变量。 + * @param second 第二个变量。 + * @param third 第三个变量。 + */ + public Tuple3(T1 first, T2 second, T3 third) { + this.first = first; + this.second = second; + this.third = third; + } + + /** + * 获取第一个变量。 + * + * @return 返回第一个变量。 + */ + public T1 getFirst() { + return first; + } + + /** + * 获取第二个变量。 + * + * @return 返回第二个变量。 + */ + public T2 getSecond() { + return second; + } + + /** + * 获取第三个变量。 + * + * @return 返回第三个变量。 + */ + public T3 getThird() { + return third; + } +} + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java new file mode 100644 index 00000000..2dea0ca3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java @@ -0,0 +1,109 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +/** + * 业务方法调用结果对象。可以同时返回具体的错误和自定义类型的数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class TypedCallResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final TypedCallResult OK = new TypedCallResult<>(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误信息描述。 + */ + private String errorMessage = null; + /** + * 在验证同时,仍然需要附加的关联数据对象。 + */ + private T data; + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static TypedCallResult create(String errorMessage) { + return errorMessage == null ? ok() : error(errorMessage); + } + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @param data 附带的数据对象。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static TypedCallResult create(String errorMessage, T data) { + return errorMessage == null ? ok(data) : error(errorMessage, data); + } + + /** + * 创建表示验证成功的对象实例。 + * + * @return 验证成功对象实例。 + */ + public static TypedCallResult ok() { + return OK; + } + + /** + * 创建表示验证成功的对象实例。 + * + * @param data 附带的数据对象。 + * @return 验证成功对象实例。 + */ + public static TypedCallResult ok(T data) { + TypedCallResult result = new TypedCallResult<>(); + result.data = data; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @return 验证失败对象实例。 + */ + public static TypedCallResult error(String errorMessage) { + TypedCallResult result = new TypedCallResult<>(); + result.success = false; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @param data 附带的数据对象。 + * @return 验证失败对象实例。 + */ + public static TypedCallResult error(String errorMessage, T data) { + TypedCallResult result = new TypedCallResult<>(); + result.success = false; + result.errorMessage = errorMessage; + result.data = data; + return result; + } + + /** + * 根据参数中出错的TypedCallResult,创建新的错误调用结果对象。 + * @param result 错误调用结果对象。 + * @return 新的错误调用结果对象。 + */ + public static TypedCallResult errorFrom(TypedCallResult result) { + return error(result.getErrorMessage()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java new file mode 100644 index 00000000..840610bf --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java @@ -0,0 +1,216 @@ +package com.orangeforms.common.core.upload; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * 上传或下载文件抽象父类。 + * 包含存储本地文件的功能,以及上传和下载所需的通用方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class BaseUpDownloader { + + /** + * 构建上传文件的完整目录。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 上传文件的完整路径名。 + */ + public String makeFullPath( + String rootBaseDir, String modelName, String fieldName, Boolean asImage) { + StringBuilder uploadPathBuilder = new StringBuilder(128); + if (StringUtils.isNotBlank(rootBaseDir)) { + uploadPathBuilder.append(rootBaseDir).append("/"); + } + if (Boolean.TRUE.equals(asImage)) { + uploadPathBuilder.append(ApplicationConstant.UPLOAD_IMAGE_PARENT_PATH); + } else { + uploadPathBuilder.append(ApplicationConstant.UPLOAD_ATTACHMENT_PARENT_PATH); + } + if (StringUtils.isNotBlank(modelName)) { + uploadPathBuilder.append("/").append(modelName); + } + if (StringUtils.isNotBlank(fieldName)) { + uploadPathBuilder.append("/").append(fieldName); + } + return uploadPathBuilder.toString(); + } + + /** + * 构建上传文件的完整目录。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param path 文件目录。 + * @return 上传文件的完整路径名。 + */ + public String makeFullPath(String rootBaseDir, String path) { + StringBuilder uploadPathBuilder = new StringBuilder(128); + if (StringUtils.isNotBlank(rootBaseDir)) { + uploadPathBuilder.append(rootBaseDir).append("/"); + } + if (StringUtils.isNotBlank(path)) { + if (!StrUtil.startWith(path, "/")) { + uploadPathBuilder.append("/"); + } + uploadPathBuilder.append(path); + } + return uploadPathBuilder.toString(); + } + + /** + * 构建上传操作的返回对象。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param originalFilename 上传文件的原始文件名(包含扩展名)。 + */ + protected void fillUploadResponseInfo( + UploadResponseInfo responseInfo, String serviceContextPath, String originalFilename) { + // 根据请求上传的uri构建下载uri,只是将末尾的/upload改为/download即可。 + HttpServletRequest request = ContextUtil.getHttpRequest(); + String uri = request.getRequestURI(); + uri = StringUtils.removeEnd(uri, "/"); + uri = StringUtils.removeEnd(uri, "/upload"); + String downloadUri; + if (StringUtils.isBlank(serviceContextPath)) { + downloadUri = uri + "/download"; + } else { + downloadUri = serviceContextPath + uri + "/download"; + } + StringBuilder filenameBuilder = new StringBuilder(64); + filenameBuilder.append(MyCommonUtil.generateUuid()) + .append(".").append(FilenameUtils.getExtension(originalFilename)); + responseInfo.setDownloadUri(downloadUri); + responseInfo.setFilename(filenameBuilder.toString()); + } + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param fileName 文件名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @param response Http 应答对象。 + * @throws IOException 操作错误。 + */ + public abstract void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) throws IOException; + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param uriPath uri中的路径名。 + * @param fileName 文件名。 + * @param response Http 应答对象。 + * @throws IOException 操作错误。 + */ + public abstract void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException; + + /** + * 执行文件上传操作,并存入本地文件系统,再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象,返回给前端。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param rootBaseDir 存放上传文件的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param uploadFile Http请求中上传的文件对象。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 存储在本地上传文件名。 + * @throws IOException 操作错误。 + */ + public abstract UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException; + + /** + * 执行文件上传操作,并存入本地文件系统,再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象,返回给前端。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param rootBaseDir 存放上传文件的根目录。 + * @param uriPath uri中的路径名。 + * @param uploadFile Http请求中上传的文件对象。 + * @return 存储在本地上传文件名。 + * @throws IOException 操作错误。 + */ + public abstract UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String uriPath, + MultipartFile uploadFile) throws IOException; + + /** + * 判断filename参数指定的文件名,是否被包含在fileInfoJson参数中。 + * + * @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。 + * @param filename 被包含的文件名。 + * @return 存在返回true,否则false。 + */ + public static boolean containFile(String fileInfoJson, String filename) { + if (StringUtils.isAnyBlank(fileInfoJson, filename)) { + return false; + } + List fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class); + if (CollectionUtils.isNotEmpty(fileInfoList)) { + for (UploadResponseInfo fileInfo : fileInfoList) { + if (StringUtils.equals(filename, fileInfo.getFilename())) { + return true; + } + } + } + return false; + } + + protected UploadResponseInfo verifyUploadArgument( + Boolean asImage, MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = new UploadResponseInfo(); + if (Objects.isNull(uploadFile) || uploadFile.isEmpty()) { + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_ARGUMENT.getErrorMessage()); + return responseInfo; + } + if (BooleanUtil.isTrue(asImage) && ImageIO.read(uploadFile.getInputStream()) == null) { + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_FORMAT.getErrorMessage()); + return responseInfo; + } + return responseInfo; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java new file mode 100644 index 00000000..e883d06e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java @@ -0,0 +1,169 @@ +package com.orangeforms.common.core.upload; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * 存储本地文件的上传下载实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class LocalUpDownloader extends BaseUpDownloader { + + @Autowired + private UpDownloaderFactory factory; + + @PostConstruct + public void doRegister() { + factory.registerUpDownloader(UploadStoreTypeEnum.LOCAL_SYSTEM, this); + } + + @Override + public void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) { + String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage); + String fullFileanme = uploadPath + "/" + fileName; + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException { + StringBuilder pathBuilder = new StringBuilder(128); + if (StrUtil.isNotBlank(rootBaseDir)) { + pathBuilder.append(rootBaseDir); + } + if (StrUtil.isNotBlank(uriPath)) { + pathBuilder.append(uriPath); + } + pathBuilder.append("/"); + String fullFileanme = pathBuilder.append(fileName).toString(); + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage); + return this.doUploadInternally(serviceContextPath, uploadPath, asImage, uploadFile); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String uriPath, + MultipartFile uploadFile) throws IOException { + String uploadPath = makeFullPath(rootBaseDir, uriPath); + return this.doUploadInternally(serviceContextPath, uploadPath, false, uploadFile); + } + + /** + * 判断filename参数指定的文件名,是否被包含在fileInfoJson参数中。 + * + * @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。 + * @param filename 被包含的文件名。 + * @return 存在返回true,否则false。 + */ + public static boolean containFile(String fileInfoJson, String filename) { + if (StringUtils.isAnyBlank(fileInfoJson, filename)) { + return false; + } + List fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class); + if (CollectionUtils.isNotEmpty(fileInfoList)) { + for (UploadResponseInfo fileInfo : fileInfoList) { + if (StringUtils.equals(filename, fileInfo.getFilename())) { + return true; + } + } + } + return false; + } + + private UploadResponseInfo doUploadInternally( + String serviceContextPath, + String uploadPath, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = super.verifyUploadArgument(asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + return responseInfo; + } + responseInfo.setUploadPath(uploadPath); + fillUploadResponseInfo(responseInfo, serviceContextPath, uploadFile.getOriginalFilename()); + try { + byte[] bytes = uploadFile.getBytes(); + StringBuilder sb = new StringBuilder(256); + sb.append(uploadPath).append("/").append(responseInfo.getFilename()); + Path path = Paths.get(sb.toString()); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(uploadPath)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } catch (IOException e) { + log.error("Failed to write uploaded file [" + uploadFile.getOriginalFilename() + " ].", e); + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_IOERROR.getErrorMessage()); + return responseInfo; + } + return responseInfo; + } + + private void downloadInternal(String fullFileanme, String fileName, HttpServletResponse response) { + File file = new File(fullFileanme); + if (!file.exists()) { + log.warn("Download file [" + fullFileanme + "] failed, no file found!"); + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + byte[] buff = new byte[2048]; + try (OutputStream os = response.getOutputStream(); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { + int i = bis.read(buff); + while (i != -1) { + os.write(buff, 0, i); + os.flush(); + i = bis.read(buff); + } + } catch (IOException e) { + log.error("Failed to call LocalUpDownloader.doDownload", e); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java new file mode 100644 index 00000000..323880d4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.core.upload; + +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.Map; + +/** + * 业务对象根据上传下载存储类型,获取上传下载对象的工厂类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class UpDownloaderFactory { + + private final Map upDownloaderMap = new EnumMap<>(UploadStoreTypeEnum.class); + + /** + * 根据存储类型获取上传下载对象。 + * @param storeType 存储类型。 + * @return 匹配的上传下载对象。 + */ + public BaseUpDownloader get(UploadStoreTypeEnum storeType) { + BaseUpDownloader upDownloader = upDownloaderMap.get(storeType); + if (upDownloader == null) { + throw new UnsupportedOperationException( + "The storeType [" + storeType.name() + "] isn't supported, please add dependency jar first."); + } + return upDownloader; + } + + /** + * 注册上传下载对象到工厂。 + * + * @param storeType 存储类型。 + * @param upDownloader 上传下载对象。 + */ + public void registerUpDownloader(UploadStoreTypeEnum storeType, BaseUpDownloader upDownloader) { + if (storeType == null || upDownloader == null) { + throw new IllegalArgumentException("The Argument can't be NULL."); + } + if (upDownloaderMap.containsKey(storeType)) { + throw new UnsupportedOperationException( + "The storeType [" + storeType.name() + "] has been registered already."); + } + upDownloaderMap.put(storeType, upDownloader); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java new file mode 100644 index 00000000..3610a541 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.core.upload; + +import lombok.Data; + +/** + * 数据上传操作的应答信息对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class UploadResponseInfo { + /** + * 上传是否出现错误。 + */ + private Boolean uploadFailed = false; + /** + * 具体错误信息。 + */ + private String errorMessage; + /** + * 返回前端的下载url。 + */ + private String downloadUri; + /** + * 上传文件所在路径。 + */ + private String uploadPath; + /** + * 返回给前端的文件名。 + */ + private String filename; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java new file mode 100644 index 00000000..32d7fed6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.core.upload; + +import lombok.Data; + +/** + * 上传数据存储信息对象。这里之所以使用对象,主要是便于今后扩展。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class UploadStoreInfo { + + /** + * 是否支持上传。 + */ + private boolean supportUpload; + /** + * 上传数据存储类型。 + */ + private UploadStoreTypeEnum storeType; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java new file mode 100644 index 00000000..62c1d2d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java @@ -0,0 +1,31 @@ +package com.orangeforms.common.core.upload; + +/** + * 上传数据存储介质类型枚举。 + * + * @author Jerry + * @date 2024-07-02 + */ +public enum UploadStoreTypeEnum { + + /** + * 本地系统。 + */ + LOCAL_SYSTEM, + /** + * minio分布式存储。 + */ + MINIO_SYSTEM, + /** + * 阿里云OSS存储。 + */ + ALIYUN_OSS_SYTEM, + /** + * 腾讯云COS存储。 + */ + QCLOUD_COS_SYTEM, + /** + * 华为云OBS存储。 + */ + HUAWEI_OBS_SYSTEM +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java new file mode 100644 index 00000000..48844678 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.ReflectUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * 获取JDK动态代理/CGLIB代理对象代理的目标对象的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class AopTargetUtil { + + /** + * 获取参数对象代理的目标对象。 + * + * @param proxy 代理对象 + * @return 代理的目标对象。 + */ + public static Object getTarget(Object proxy) { + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + try { + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } else { + return getCglibProxyTargetObject(proxy); + } + } catch (Exception e) { + log.error("Failed to call getJdkDynamicProxyTargetObject or getCglibProxyTargetObject", e); + return null; + } + } + + /** + * 获取被织入完整的方法名。 + * + * @param joinPoint 织入方法对象。 + * @return 被织入完整的方法名。 + */ + public static String getFullMethodName(ProceedingJoinPoint joinPoint) { + StringBuilder sb = new StringBuilder(512); + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + sb.append(methodSignature.getMethod().getName()).append("("); + String paramTypes = Arrays.stream(methodSignature.getParameterTypes()) + .map(Class::getSimpleName).collect(Collectors.joining(", ")); + sb.append(paramTypes).append(")"); + return sb.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AopTargetUtil() { + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0"); + Object dynamicAdvisedInterceptor = ReflectUtil.getFieldValue(proxy, h); + Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised"); + return ((AdvisedSupport) ReflectUtil.getFieldValue(dynamicAdvisedInterceptor, advised)).getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + Field h = proxy.getClass().getSuperclass().getDeclaredField("h"); + AopProxy aopProxy = (AopProxy) ReflectUtil.getFieldValue(proxy, h); + Field advised = aopProxy.getClass().getDeclaredField("advised"); + return ((AdvisedSupport) ReflectUtil.getFieldValue(aopProxy, advised)).getTargetSource().getTarget(); + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java new file mode 100644 index 00000000..2a53c923 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java @@ -0,0 +1,88 @@ +package com.orangeforms.common.core.util; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * Spring 系统启动应用感知对象,主要用于获取Spring Bean的上下文对象,后续的代码中可以直接查找系统中加载的Bean对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class ApplicationContextHolder implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + /** + * Spring 启动的过程中会自动调用,并将应用上下文对象赋值进来。 + * + * @param applicationContext 应用上下文对象,可通过该对象查找Spring中已经加载的Bean。 + */ + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) { + doSetApplicationContext(applicationContext); + } + + /** + * 获取应用上下文对象。 + * + * @return 应用上下文。 + */ + public static ApplicationContext getApplicationContext() { + assertApplicationContext(); + return applicationContext; + } + + /** + * 根据BeanName,获取Bean对象。 + * + * @param beanName Bean名称。 + * @param 返回的Bean类型。 + * @return Bean对象。 + */ + @SuppressWarnings("unchecked") + public static T getBean(String beanName) { + assertApplicationContext(); + return (T) applicationContext.getBean(beanName); + } + + /** + * 根据Bean的ClassType,获取Bean对象。 + * + * @param beanType Bean的Class类型。 + * @param 返回的Bean类型。 + * @return Bean对象。 + */ + public static T getBean(Class beanType) { + assertApplicationContext(); + return applicationContext.getBean(beanType); + } + + /** + * 根据Bean的ClassType,获取Bean对象列表。 + * + * @param beanType Bean的Class类型。 + * @param 返回的Bean类型。 + * @return Bean对象列表。 + */ + public static Collection getBeanListOfType(Class beanType) { + assertApplicationContext(); + return applicationContext.getBeansOfType(beanType).values(); + } + + private static void assertApplicationContext() { + if (ApplicationContextHolder.applicationContext == null) { + throw new MyRuntimeException("applicaitonContext属性为null,请检查是否注入了ApplicationContextHolder!"); + } + } + + private static void doSetApplicationContext(ApplicationContext applicationContext) { + ApplicationContextHolder.applicationContext = applicationContext; + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java new file mode 100644 index 00000000..95382bde --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java @@ -0,0 +1,51 @@ +package com.orangeforms.common.core.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 获取Servlet HttpRequest和HttpResponse的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class ContextUtil { + + /** + * 判断当前是否处于HttpServletRequest上下文环境。 + * + * @return 是返回true,否则false。 + */ + public static boolean hasRequestContext() { + return RequestContextHolder.getRequestAttributes() != null; + } + + /** + * 获取Servlet请求上下文的HttpRequest对象。 + * + * @return 请求上下文中的HttpRequest对象。 + */ + public static HttpServletRequest getHttpRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes == null ? null : attributes.getRequest(); + } + + /** + * 获取Servlet请求上下文的HttpResponse对象。 + * + * @return 请求上下文中的HttpResponse对象。 + */ + public static HttpServletResponse getHttpResponse() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes == null ? null : attributes.getResponse(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ContextUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java new file mode 100644 index 00000000..256ddf5a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.util; + +/** + * 基于自定义解析规则的多数据源解析接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface DataSourceResolver { + + /** + * 动态解析方法。实现类可以根据当前的请求,或者上下文环境进行动态解析。 + * + * @param arg 可选的入参。MyDataSourceResolver注解中的arg参数。 + * @param intArg 可选的整型入参。MyDataSourceResolver注解中的intArg参数。 + * @param methodName 被织入方法名称。 + * @param methodArgs 被织入方法的所有参数。 + * @return 返回用于多数据源切换的类型值。DataSourceResolveAspect 切面方法会根据该返回值和配置信息,进行多数据源切换。 + */ + Integer resolve(String arg, Integer intArg, String methodName, Object[] methodArgs); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java new file mode 100644 index 00000000..b11e16fc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java @@ -0,0 +1,55 @@ +package com.orangeforms.common.core.util; + +import org.springframework.stereotype.Component; + +/** + * 常量值指向的数据源。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class DefaultDataSourceResolver implements DataSourceResolver { + + private static final ThreadLocal DEFAULT_CONTEXT_HOLDER = new ThreadLocal<>(); + + @Override + public Integer resolve(String arg, Integer intArg, String methodName, Object[] methodArgs) { + Integer datasourceType = DEFAULT_CONTEXT_HOLDER.get(); + return datasourceType != null ? datasourceType : intArg; + } + + /** + * 设置报表数据源类型值。 + * + * @param type 数据源类型 + * @return 原有数据源类型,如果第一次设置则返回null。 + */ + public static Integer setDataSourceType(Integer type) { + Integer datasourceType = DEFAULT_CONTEXT_HOLDER.get(); + DEFAULT_CONTEXT_HOLDER.set(type); + return datasourceType; + } + + /** + * 获取当前报表数据库操作执行线程的数据源类型,同时由动态数据源的路由函数调用。 + * + * @return 数据源类型。 + */ + public static Integer getDataSourceType() { + return DEFAULT_CONTEXT_HOLDER.get(); + } + + /** + * 清除线程本地变量,以免内存泄漏。 + + * @param originalType 原有的数据源类型,如果该值为null,则情况本地化变量。 + */ + public static void unset(Integer originalType) { + if (originalType == null) { + DEFAULT_CONTEXT_HOLDER.remove(); + } else { + DEFAULT_CONTEXT_HOLDER.set(originalType); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java new file mode 100644 index 00000000..b3d37aa8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java @@ -0,0 +1,111 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.ExcelWriter; +import cn.jimmyshi.beanquery.BeanQuery; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.MyRuntimeException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.io.FilenameUtils; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 导出工具类,目前支持xlsx和csv两种类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class ExportUtil { + + /** + * 数据导出。目前仅支持xlsx和csv。 + * + * @param dataList 导出数据列表。 + * @param selectFieldMap 导出的数据字段,key为对象字段名称,value为中文标题名称。 + * @param filename 导出文件名。 + * @param 数据对象类型。 + * @throws IOException 文件操作失败。 + */ + public static void doExport( + Collection dataList, Map selectFieldMap, String filename) throws IOException { + if (CollUtil.isEmpty(dataList)) { + return; + } + StringBuilder sb = new StringBuilder(128); + for (Map.Entry e : selectFieldMap.entrySet()) { + sb.append(e.getKey()).append(" as ").append(e.getValue()).append(", "); + } + // 去掉末尾的逗号 + String selectFieldString = sb.substring(0, sb.length() - 2); + // 写出数据到xcel格式的输出流 + List> resultList = BeanQuery.select(selectFieldString).executeFrom(dataList); + normalizeMultiSelectList(resultList); + // 构建HTTP输出流参数 + HttpServletResponse response = ContextUtil.getHttpResponse(); + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + filename); + if (ApplicationConstant.XLSX_EXT.equals(FilenameUtils.getExtension(filename))) { + ServletOutputStream out = response.getOutputStream(); + ExcelWriter writer = ExcelUtil.getWriter(true); + writer.setRowHeight(-1, 30); + writer.setColumnWidth(-1, 30); + writer.setColumnWidth(1, 20); + writer.write(resultList); + writer.flush(out); + writer.close(); + IoUtil.close(out); + } else if (ApplicationConstant.CSV_EXT.equals(FilenameUtils.getExtension(filename))) { + Collection headerList = selectFieldMap.values(); + String[] headerArray = new String[headerList.size()]; + headerList.toArray(headerArray); + CSVFormat format = CSVFormat.DEFAULT.withHeader(headerArray); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + try (Writer out = response.getWriter(); CSVPrinter printer = new CSVPrinter(out, format)) { + for (Map o : resultList) { + for (Map.Entry entry : o.entrySet()) { + printer.print(entry.getValue()); + } + printer.println(); + } + printer.flush(); + } catch (Exception e) { + log.error("Failed to call ExportUtil.doExport", e); + } + } else { + throw new MyRuntimeException("不支持的导出文件类型!"); + } + } + + @SuppressWarnings("unchecked") + private static void normalizeMultiSelectList(List> resultList) { + for (Map data : resultList) { + for (Map.Entry entry : data.entrySet()) { + if (entry.getValue() instanceof List) { + List> dictMapList = ((List>) entry.getValue()); + List nameList = dictMapList.stream() + .map(item -> item.get("name").toString()).collect(Collectors.toList()); + data.put(entry.getKey(), CollUtil.join(nameList, ",")); + } + } + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ExportUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java new file mode 100644 index 00000000..7b17f596 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java @@ -0,0 +1,356 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.sax.handler.RowHandler; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationDict; +import com.orangeforms.common.core.annotation.RelationGlobalDict; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.exception.MyRuntimeException; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 导入工具类,目前支持xlsx和xls两种类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class ImportUtil { + + /** + * 根据实体类的Class类型,生成导入的头信息。 + * + * @param modelClazz 实体对象的Class类型。 + * @param ignoreFields 忽略的字段名集合,如创建时间、创建人、更新时间、更新人等。 + * @param 实体对象类型。 + * @return 创建后的导入头信息列表。 + */ + public static List makeHeaderInfoList(Class modelClazz, Set ignoreFields) { + List resultList = new LinkedList<>(); + Field[] fields = ReflectUtil.getFields(modelClazz); + int index = 0; + for (Field field : fields) { + int modifiers = field.getModifiers(); + // transient类型的字段不能作为查询条件,静态字段和逻辑删除都不考虑。需要忽略的字段也要跳过。 + int transientMask = 128; + if ((modifiers & transientMask) == 1 + || Modifier.isStatic(modifiers) + || field.getAnnotation(Id.class) != null + || isLogicDeleteColumn(field) + || CollUtil.contains(ignoreFields, field.getName())) { + continue; + } + Column tableField = field.getAnnotation(Column.class); + if (tableField == null || !tableField.ignore()) { + ImportHeaderInfo headerInfo = new ImportHeaderInfo(); + headerInfo.fieldName = field.getName(); + headerInfo.index = index++; + makeHeaderInfoFieldTypeByField(field, headerInfo); + resultList.add(headerInfo); + } + } + return resultList; + } + + private static boolean isLogicDeleteColumn(Field field) { + Column c = field.getAnnotation(Column.class); + return c != null && c.isLogicDelete(); + } + + /** + * 保存导入文件。 + * + * @param baseDir 导入文件本地缓存的根目录。 + * @param subDir 导入文件本地缓存的子目录。 + * @param importFile 导入的文件。 + * @return 保存的本地文件名。 + */ + public static String saveImportFile( + String baseDir, String subDir, MultipartFile importFile) throws IOException { + StringBuilder sb = new StringBuilder(256); + sb.append(baseDir); + if (!StrUtil.endWith(baseDir, "/")) { + sb.append("/"); + } + sb.append("importedFile/"); + if (StrUtil.isNotBlank(subDir)) { + sb.append(subDir); + if (!StrUtil.endWith(subDir, "/")) { + sb.append("/"); + } + } + String pathname = sb.toString(); + sb.append(new DateTime().toString("yyyy-MM-dd-HH-mm-")); + sb.append(MyCommonUtil.generateUuid()) + .append(".").append(FileNameUtil.getSuffix(importFile.getOriginalFilename())); + String fullname = sb.toString(); + try { + byte[] bytes = importFile.getBytes(); + Path path = Paths.get(fullname); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(pathname)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } catch (IOException e) { + log.error("Failed to write imported file [" + importFile.getOriginalFilename() + " ].", e); + throw e; + } + return fullname; + } + + /** + * 导入指定的excel,基于SAX方式解析后返回数据列表。 + * + * @param headers 头信息数组。 + * @param skipHeader 是否跳过第一行,通常改行为头信息。 + * @param filename 文件名。 + * @return 解析后数据列表。 + */ + public static List> doImport( + ImportHeaderInfo[] headers, boolean skipHeader, String filename) { + Assert.notNull(headers); + Assert.isTrue(StrUtil.isNotBlank(filename)); + List> resultList = new LinkedList<>(); + ExcelUtil.readBySax(new File(filename), 0, createRowHandler(headers, skipHeader, resultList)); + return resultList; + } + + /** + * 导入指定的excel,基于SAX方式解析后返回Bean类型的数据列表。 + * + * @param headers 头信息数组。 + * @param skipHeader 是否跳过第一行,通常改行为头信息。 + * @param filename 文件名。 + * @param clazz Bean的Class类型。 + * @param translateDictFieldSet 需要进行反向翻译的字典字段集合。 + * @return 解析后数据列表。 + */ + public static List doImport( + ImportHeaderInfo[] headers, + boolean skipHeader, + String filename, + Class clazz, + Set translateDictFieldSet) { + // 这里将需要进行字典反向翻译的字段类型改为String,否则使用原有的字典Id类型时,无法正确执行下面的doImport方法。 + if (CollUtil.isNotEmpty(translateDictFieldSet)) { + for (ImportHeaderInfo header : headers) { + if (translateDictFieldSet.contains(header.fieldName)) { + header.fieldType = STRING_TYPE; + } + } + } + List> resultList = doImport(headers, skipHeader, filename); + if (CollUtil.isNotEmpty(translateDictFieldSet)) { + translateDictFieldSet.forEach(c -> doTranslateDict(resultList, clazz, c)); + } + return MyModelUtil.mapToBeanList(resultList, clazz); + } + + /** + * 转换数据列表中,需要进行反向字典翻译的字段。 + * + * @param dataList 数据列表。 + * @param modelClass 对象模型。 + * @param fieldName 需要进行字典反向翻译的字段名。注意,该字段为需要翻译替换的Java字段名,与此同时, + * 该字段 + DictMap后缀的字段名,必须被RelationConstDict和RelationDict注解标记。 + */ + @SuppressWarnings("unchecked") + public static void doTranslateDict(List> dataList, Class modelClass, String fieldName) { + if (CollUtil.isEmpty(dataList)) { + return; + } + Field field = ReflectUtil.getField(modelClass, fieldName + "DictMap"); + Assert.notNull(field); + Map inversedDictMap; + if (field.isAnnotationPresent(RelationConstDict.class)) { + RelationConstDict r = field.getAnnotation(RelationConstDict.class); + Field f = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = (Map) ReflectUtil.getStaticFieldValue(f); + inversedDictMap = MapUtil.inverse(dictMap); + } else if (field.isAnnotationPresent(RelationDict.class)) { + RelationDict r = field.getAnnotation(RelationDict.class); + String slaveServiceName = r.slaveServiceName(); + if (StrUtil.isBlank(slaveServiceName)) { + slaveServiceName = r.slaveModelClass().getSimpleName() + "Service"; + } + BaseService service = + ApplicationContextHolder.getBean(StrUtil.lowerFirst(slaveServiceName)); + List dictDataList = service.getAllList(); + List> dataMapList = MyModelUtil.beanToMapList(dictDataList); + inversedDictMap = new HashMap<>(dataMapList.size()); + dataMapList.forEach(d -> + inversedDictMap.put(d.get(r.slaveNameField()).toString(), d.get(r.slaveIdField()))); + } else if (field.isAnnotationPresent(RelationGlobalDict.class)) { + RelationGlobalDict r = field.getAnnotation(RelationGlobalDict.class); + BaseService s = ApplicationContextHolder.getBean("globalDictService"); + Method m = ReflectUtil.getMethodByName(s.getClass(), "getGlobalDictItemDictMapFromCache"); + Map dictMap = ReflectUtil.invoke(s, m, r.dictCode(), null); + inversedDictMap = MapUtil.inverse(dictMap); + } else { + throw new UnsupportedOperationException("Only Support RelationConstDict and RelationDict Field"); + } + if (MapUtil.isEmpty(inversedDictMap)) { + log.warn("Dict Data List is EMPTY."); + return; + } + for (Map data : dataList) { + Object value = data.get(fieldName); + if (value != null) { + Object newValue = inversedDictMap.get(value.toString()); + if (newValue != null) { + data.put(fieldName, newValue); + } + } + } + } + + private static void makeHeaderInfoFieldTypeByField(Field field, ImportHeaderInfo headerInfo) { + if (field.getType().equals(Integer.class)) { + headerInfo.fieldType = INT_TYPE; + } else if (field.getType().equals(Long.class)) { + headerInfo.fieldType = LONG_TYPE; + } else if (field.getType().equals(String.class)) { + headerInfo.fieldType = STRING_TYPE; + } else if (field.getType().equals(Boolean.class)) { + headerInfo.fieldType = BOOLEAN_TYPE; + } else if (field.getType().equals(Date.class)) { + headerInfo.fieldType = DATE_TYPE; + } else if (field.getType().equals(Double.class)) { + headerInfo.fieldType = DOUBLE_TYPE; + } else if (field.getType().equals(Float.class)) { + headerInfo.fieldType = FLOAT_TYPE; + } else if (field.getType().equals(BigDecimal.class)) { + headerInfo.fieldType = BIG_DECIMAL_TYPE; + } else { + throw new MyRuntimeException("Unsupport Import FieldType"); + } + } + + private static RowHandler createRowHandler( + ImportHeaderInfo[] headers, boolean skipHeader, List> resultList) { + return new MyRowHandler(headers, skipHeader, resultList); + } + + public static final int INT_TYPE = 0; + public static final int LONG_TYPE = 1; + public static final int STRING_TYPE = 2; + public static final int BOOLEAN_TYPE = 3; + public static final int DATE_TYPE = 4; + public static final int DOUBLE_TYPE = 5; + public static final int FLOAT_TYPE = 6; + public static final int BIG_DECIMAL_TYPE = 7; + + @NoArgsConstructor + @AllArgsConstructor + @Data + public static class ImportHeaderInfo { + /** + * 对应的Java实体对象属性名。 + */ + private String fieldName; + /** + * 对应的Java实体对象类型。 + */ + private Integer fieldType; + /** + * 0 表示excel中的第一列。 + */ + private Integer index; + } + + private static class MyRowHandler implements RowHandler { + private ImportHeaderInfo[] headers; + private Map headerInfoMap; + private boolean skipHeader; + private List> resultList; + + public MyRowHandler(ImportHeaderInfo[] headers, boolean skipHeader, List> resultList) { + this.headers = headers; + this.skipHeader = skipHeader; + this.resultList = resultList; + this.headerInfoMap = Arrays.stream(headers) + .collect(Collectors.toMap(ImportHeaderInfo::getIndex, c -> c)); + } + + @Override + public void handle(int sheetIndex, long rowIndex, List rowList) { + if (this.skipHeader && rowIndex == 0) { + return; + } + int i = 0; + Map data = new HashMap<>(headers.length); + for (Object rowData : rowList) { + ImportHeaderInfo headerInfo = this.headerInfoMap.get(i++); + if (headerInfo == null) { + continue; + } + switch (headerInfo.fieldType) { + case INT_TYPE: + data.put(headerInfo.fieldName, Convert.toInt(rowData)); + break; + case LONG_TYPE: + data.put(headerInfo.fieldName, Convert.toLong(rowData)); + break; + case STRING_TYPE: + data.put(headerInfo.fieldName, Convert.toStr(rowData)); + break; + case BOOLEAN_TYPE: + data.put(headerInfo.fieldName, Convert.toBool(rowData)); + break; + case DATE_TYPE: + data.put(headerInfo.fieldName, Convert.toDate(rowData)); + break; + case DOUBLE_TYPE: + data.put(headerInfo.fieldName, Convert.toDouble(rowData)); + break; + case FLOAT_TYPE: + data.put(headerInfo.fieldName, Convert.toFloat(rowData)); + break; + case BIG_DECIMAL_TYPE: + data.put(headerInfo.fieldName, Convert.toBigDecimal(rowData)); + break; + default: + throw new MyRuntimeException( + "Invalid ImportHeaderInfo.fieldType [" + headerInfo.fieldType + "]."); + } + } + resultList.add(data); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ImportUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java new file mode 100644 index 00000000..c9ac471f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java @@ -0,0 +1,104 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * Ip工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class IpUtil { + + private static final String UNKNOWN = "unknown"; + + /** + * 通过Servlet的HttpRequest对象获取Ip地址。 + * + * @param request HttpRequest对象。 + * @return 本次请求的Ip地址。 + */ + public static String getRemoteIpAddress(HttpServletRequest request) { + String ip = null; + // X-Forwarded-For:Squid 服务代理 + String ipAddresses = request.getHeader("X-Forwarded-For"); + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // Proxy-Client-IP:apache 服务代理 + ipAddresses = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // WL-Proxy-Client-IP:weblogic 服务代理 + ipAddresses = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // HTTP_CLIENT_IP:有些代理服务器 + ipAddresses = request.getHeader("HTTP_CLIENT_IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // X-Real-IP:nginx服务代理 + ipAddresses = request.getHeader("X-Real-IP"); + } + // 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP + if (StrUtil.isNotBlank(ipAddresses)) { + ip = ipAddresses.split(",")[0]; + } + // 还是不能获取到,最后再通过request.getRemoteAddr();获取 + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + public static String getFirstLocalIpAddress() { + String ip; + try { + List ipList = getHostAddress(); + // default the first + ip = (!ipList.isEmpty()) ? ipList.get(0) : ""; + } catch (Exception ex) { + ip = ""; + log.error("Failed to call ", ex); + } + return ip; + } + + private static List getHostAddress() throws SocketException { + List ipList = new ArrayList<>(5); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + Enumeration allAddress = ni.getInetAddresses(); + while (allAddress.hasMoreElements()) { + InetAddress address = allAddress.nextElement(); + // skip the IPv6 addr + // skip the IPv6 addr + if (address.isLoopbackAddress() || address instanceof Inet6Address) { + continue; + } + String hostAddress = address.getHostAddress(); + ipList.add(hostAddress); + } + } + return ipList; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private IpUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java new file mode 100644 index 00000000..84e23a06 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java @@ -0,0 +1,112 @@ +package com.orangeforms.common.core.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.Map; + +/** + * 基于JWT的Token生成工具类 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class JwtUtil { + + private static final String TOKEN_PREFIX = "Bearer "; + private static final String CLAIM_KEY_CREATEDTIME = "CreatedTime"; + + /** + * Token缺省过期时间是30分钟 + */ + private static final Long TOKEN_EXPIRATION = 1800000L; + /** + * 缺省情况下,Token会每5分钟被刷新一次 + */ + private static final Long REFRESH_TOKEN_INTERVAL = 300000L; + + /** + * 生成加密后的JWT令牌,生成的结果中包含令牌前缀,如"Bearer " + * + * @param claims 令牌中携带的数据 + * @param expirationMillisecond 过期的毫秒数 + * @return 生成后的令牌信息 + */ + public static String generateToken(Map claims, long expirationMillisecond, String signingKey) { + // 自动添加token的创建时间 + long createTime = System.currentTimeMillis(); + claims.put(CLAIM_KEY_CREATEDTIME, createTime); + SecretKey sk = Keys.hmacShaKeyFor(signingKey.getBytes()); + String token = Jwts.builder().claims(claims) + .signWith(sk, Jwts.SIG.HS256) + .expiration(new Date(createTime + expirationMillisecond)) + .compact(); + return TOKEN_PREFIX + token; + } + + /** + * 生成加密后的JWT令牌,生成的结果中包含令牌前缀,如"Bearer " + * + * @param claims 令牌中携带的数据 + * @return 生成后的令牌信息 + */ + public static String generateToken(Map claims, String signingKey) { + return generateToken(claims, TOKEN_EXPIRATION, signingKey); + } + + /** + * 获取token中的数据对象 + * + * @param token 令牌信息(需要包含令牌前缀,如"Bearer ") + * @return 令牌中的数据对象,解析视频返回null。 + */ + public static Claims parseToken(String token, String signingKey) { + if (token == null || !token.startsWith(TOKEN_PREFIX)) { + return null; + } + String tokenKey = token.substring(TOKEN_PREFIX.length()); + Claims claims = null; + try { + SecretKey sk = Keys.hmacShaKeyFor(signingKey.getBytes()); + claims = Jwts.parser().verifyWith(sk).build().parseSignedClaims(tokenKey).getPayload(); + } catch (Exception e) { + log.error("Token Expired", e); + } + return claims; + } + + /** + * 判断令牌是否过期 + * + * @param claims 令牌解密后的Map对象。 + * @return true 过期,否则false。 + */ + public static boolean isNullOrExpired(Claims claims) { + return claims == null || claims.getExpiration().before(new Date()); + } + + /** + * 判断解密后的Token payload是否需要被强制刷新,如果需要,则调用generateToken方法重新生成Token。 + * + * @param claims Token解密后payload数据 + * @return true 需要刷新,否则false + */ + public static boolean needToRefresh(Claims claims) { + if (claims == null) { + return false; + } + Long createTime = (Long) claims.get(CLAIM_KEY_CREATEDTIME); + return createTime == null || System.currentTimeMillis() - createTime > REFRESH_TOKEN_INTERVAL; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private JwtUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java new file mode 100644 index 00000000..b89dd09b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.core.util; + +/** + * 拼接日志消息的工具类。 + * 主要目标是,尽量保证日志输出的统一性,同时也可以有效减少与日志信息相关的常量字符串, + * 提高代码的规范度和可维护性。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class LogMessageUtil { + + /** + * RPC调用错误格式。 + */ + private static final String RPC_ERROR_MSG_FORMAT = "RPC Failed with Error message [%s]"; + + /** + * 组装RPC调用的错误信息。 + * + * @param errorMsg 具体的错误信息。 + * @return 格式化后的错误信息。 + */ + public static String makeRpcError(String errorMsg) { + return String.format(RPC_ERROR_MSG_FORMAT, errorMsg); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private LogMessageUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java new file mode 100644 index 00000000..e1d3bc4b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.util; + +/** + * 自定义脱敏处理器接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface MaskFieldHandler { + + /** + * 处理自定义的脱敏数据。可以根据表名和字段名,使用不同的自定义脱敏规则。 + * + * @param modelName 脱敏字段所在实体对象名。 + * @param fieldName 脱敏实体对象名中的字段属性名。 + * @param data 待脱敏的数据。 + * @param maskChar 脱敏掩码字符。 + * @return 脱敏后的数据。 + */ + String handleMask(String modelName, String fieldName, String data, char maskChar); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java new file mode 100644 index 00000000..830aa2ff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java @@ -0,0 +1,203 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 脱敏的工具类。具体实现的源码基本来自hutool的DesensitizedUtil, + * 只是因为我们需要支持自定义脱敏字符,因此需要重写hutool中的工具类方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MaskFieldUtil { + + /** + * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**。 + * + * @param fullName 姓名。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的姓名。 + */ + public static String chineseName(String fullName, char maskChar) { + if (StrUtil.isBlank(fullName)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(fullName, 1, fullName.length(), maskChar); + } + + /** + * 【身份证号】前1位 和后2位。 + * + * @param idCardNum 身份证。 + * @param front 保留:前面的front位数;从1开始。 + * @param end 保留:后面的end位数;从1开始。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的身份证。 + */ + public static String idCardNum(String idCardNum, int front, int end, char maskChar) { + return noMaskPrefixAndSuffix(idCardNum, front, end, maskChar); + } + + /** + * 字符串的前front位和后end位的字符,不会被脱敏。 + * + * @param str 原字符串。 + * @param front 保留:前面的front位数;从1开始。 + * @param end 保留:后面的end位数;从1开始。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的结果字符串。 + */ + public static String noMaskPrefixAndSuffix(String str, int front, int end, char maskChar) { + //身份证不能为空 + if (StrUtil.isBlank(str)) { + return StrUtil.EMPTY; + } + //需要截取的长度不能大于身份证号长度 + if ((front + end) > str.length()) { + return StrUtil.EMPTY; + } + //需要截取的不能小于0 + if (front < 0 || end < 0) { + return StrUtil.EMPTY; + } + return StrUtil.replace(str, front, str.length() - end, maskChar); + } + + /** + * 【固定电话 前四位,后两位。 + * + * @param num 固定电话。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的固定电话。 + */ + public static String fixedPhone(String num, char maskChar) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(num, 4, num.length() - 2, maskChar); + } + + /** + * 【手机号码】前三位,后4位,其他隐藏,比如135****2210。 + * + * @param num 移动电话。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的移动电话。 + */ + public static String mobilePhone(String num, char maskChar) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(num, 3, num.length() - 4, maskChar); + } + + /** + * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****。 + * + * @param address 家庭住址。 + * @param sensitiveSize 敏感信息长度。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的家庭地址。 + */ + public static String address(String address, int sensitiveSize, char maskChar) { + if (StrUtil.isBlank(address)) { + return StrUtil.EMPTY; + } + int length = address.length(); + return StrUtil.replace(address, length - sensitiveSize, length, maskChar); + } + + /** + * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com。 + * + * @param email 邮箱。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的邮箱。 + */ + public static String email(String email, char maskChar) { + if (StrUtil.isBlank(email)) { + return StrUtil.EMPTY; + } + int index = StrUtil.indexOf(email, '@'); + if (index <= 1) { + return email; + } + return StrUtil.replace(email, 1, index, maskChar); + } + + /** + * 【密码】密码的全部字符都用*代替,比如:******。 + * + * @param password 密码。 + * @return 脱敏后的密码。 + */ + public static String password(String password) { + if (StrUtil.isBlank(password)) { + return StrUtil.EMPTY; + } + return StrUtil.repeat('*', password.length()); + } + + /** + * 【中国车牌】车牌中间用*代替。 + * eg1:null -》 "" + * eg1:"" -》 "" + * eg3:苏D40000 -》 苏D4***0 + * eg4:陕A12345D -》 陕A1****D + * eg5:京A123 -》 京A123 如果是错误的车牌,不处理。 + * + * @param carLicense 完整的车牌号。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的车牌。 + */ + public static String carLicense(String carLicense, char maskChar) { + if (StrUtil.isBlank(carLicense)) { + return StrUtil.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) { + carLicense = StrUtil.replace(carLicense, 3, 6, maskChar); + } else if (carLicense.length() == 8) { + // 新能源车牌 + carLicense = StrUtil.replace(carLicense, 3, 7, maskChar); + } + return carLicense; + } + + /** + * 银行卡号脱敏。 + * eg: 1101 **** **** **** 3256。 + * + * @param bankCardNo 银行卡号。 + * @param maskChar 遮掩字符。 + * @return 脱敏之后的银行卡号。 + */ + public static String bankCard(String bankCardNo, char maskChar) { + if (StrUtil.isBlank(bankCardNo)) { + return bankCardNo; + } + bankCardNo = StrUtil.trim(bankCardNo); + if (bankCardNo.length() < 9) { + return bankCardNo; + } + final int length = bankCardNo.length(); + final int midLength = length - 8; + final StringBuilder buf = new StringBuilder(); + buf.append(bankCardNo, 0, 4); + for (int i = 0; i < midLength; ++i) { + if (i % 4 == 0) { + buf.append(CharUtil.SPACE); + } + buf.append(maskChar); + } + buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); + return buf.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MaskFieldUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java new file mode 100644 index 00000000..fa97c514 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java @@ -0,0 +1,442 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.orangeforms.common.core.constant.AppDeviceType; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.groups.Default; +import java.lang.reflect.Field; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 脚手架中常用的基本工具方法集合,一般而言工程内部使用的方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyCommonUtil { + + private static final Validator VALIDATOR; + + static { + VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + } + + /** + * 创建uuid。 + * + * @return 返回uuid。 + */ + public static String generateUuid() { + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 对用户密码进行加盐后加密。 + * + * @param password 明文密码。 + * @param passwordSalt 盐值。 + * @return 加密后的密码。 + */ + public static String encrptedPassword(String password, String passwordSalt) { + return DigestUtil.md5Hex(password + passwordSalt); + } + + /** + * 这个方法一般用于Controller对于入口参数的基本验证。 + * 对于字符串,如果为空字符串,也将视为Blank,同时返回true。 + * + * @param objs 一组参数。 + * @return 返回是否存在null或空字符串的参数。 + */ + public static boolean existBlankArgument(Object...objs) { + for (Object obj : objs) { + if (MyCommonUtil.isBlankOrNull(obj)) { + return true; + } + } + return false; + } + + /** + * 结果和 existBlankArgument 相反。 + * + * @param objs 一组参数。 + * @return 返回是否存在null或空字符串的参数。 + */ + public static boolean existNotBlankArgument(Object...objs) { + for (Object obj : objs) { + if (!MyCommonUtil.isBlankOrNull(obj)) { + return true; + } + } + return false; + } + + /** + * 验证参数是否为空。 + * + * @param obj 待判断的参数。 + * @return 空或者null返回true,否则false。 + */ + public static boolean isBlankOrNull(Object obj) { + if (obj instanceof Collection) { + return CollUtil.isEmpty((Collection) obj); + } + return obj == null || (obj instanceof CharSequence && StrUtil.isBlank((CharSequence) obj)); + } + + /** + * 验证参数是否为非空。 + * + * @param obj 待判断的参数。 + * @return 空或者null返回false,否则true。 + */ + public static boolean isNotBlankOrNull(Object obj) { + return !isBlankOrNull(obj); + } + + /** + * 判断source是否等于其中任何一个对象值。 + * + * @param source 源对象。 + * @param others 其他对象。 + * @return 等于其中任何一个返回true,否则false。 + */ + public static boolean equalsAny(Object source, Object...others) { + for (Object one : others) { + if (ObjectUtil.equal(source, one)) { + return true; + } + } + return false; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param model 带校验的model。 + * @param groups Validate绑定的校验组。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(T model, Class...groups) { + if (model != null) { + Set> constraintViolations = VALIDATOR.validate(model, groups); + if (!constraintViolations.isEmpty()) { + Iterator> it = constraintViolations.iterator(); + ConstraintViolation constraint = it.next(); + return constraint.getMessage(); + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param model 带校验的model。 + * @param forUpdate 是否为更新。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(T model, boolean forUpdate) { + if (model != null) { + Set> constraintViolations; + if (forUpdate) { + constraintViolations = VALIDATOR.validate(model, Default.class, UpdateGroup.class); + } else { + constraintViolations = VALIDATOR.validate(model, Default.class, AddGroup.class); + } + if (!constraintViolations.isEmpty()) { + Iterator> it = constraintViolations.iterator(); + ConstraintViolation constraint = it.next(); + return constraint.getMessage(); + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param modelList 带校验的model列表。 + * @param groups Validate绑定的校验组。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(List modelList, Class... groups) { + if (CollUtil.isNotEmpty(modelList)) { + for (T model : modelList) { + String errorMessage = getModelValidationError(model, groups); + if (StrUtil.isNotBlank(errorMessage)) { + return errorMessage; + } + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param modelList 带校验的model列表。 + * @param forUpdate 是否为更新。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(List modelList, boolean forUpdate) { + if (CollUtil.isNotEmpty(modelList)) { + for (T model : modelList) { + String errorMessage = getModelValidationError(model, forUpdate); + if (StrUtil.isNotBlank(errorMessage)) { + return errorMessage; + } + } + } + return null; + } + + /** + * 拼接参数中的字符串列表,用指定分隔符进行分割,同时每个字符串对象用单引号括起来。 + * + * @param dataList 字符串集合。 + * @param separator 分隔符。 + * @return 拼接后的字符串。 + */ + public static String joinString(Collection dataList, final char separator) { + int index = 0; + StringBuilder sb = new StringBuilder(128); + for (String data : dataList) { + sb.append("'").append(data).append("'"); + if (index++ != dataList.size() - 1) { + sb.append(separator); + } + } + return sb.toString(); + } + + /** + * 将SQL Like中的通配符替换为字符本身的含义,以便于比较。 + * + * @param str 待替换的字符串。 + * @return 替换后的字符串。 + */ + public static String replaceSqlWildcard(String str) { + if (StrUtil.isBlank(str)) { + return str; + } + return StrUtil.replaceChars(StrUtil.replaceChars(str, "_", "\\_"), "%", "\\%"); + } + + /** + * 获取对象中,非空字段的名字列表。 + * + * @param object 数据对象。 + * @param clazz 数据对象的class类型。 + * @param 数据对象类型。 + * @return 数据对象中,值不为NULL的字段数组。 + */ + public static String[] getNotNullFieldNames(T object, Class clazz) { + Field[] fields = ReflectUtil.getFields(clazz); + List fieldNameList = Arrays.stream(fields) + .filter(f -> ReflectUtil.getFieldValue(object, f) != null) + .map(Field::getName).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(fieldNameList)) { + return fieldNameList.toArray(new String[]{}); + } + return new String[]{}; + } + + /** + * 获取请求头中的设备信息。 + * + * @return 设备类型,具体值可参考AppDeviceType常量类。 + */ + public static int getDeviceType() { + // 缺省都按照Web登录方式设置,如果前端header中的值为不合法值,这里也不会报错,而是使用Web缺省方式。 + int deviceType = AppDeviceType.WEB; + String deviceTypeString = ContextUtil.getHttpRequest().getHeader("deviceType"); + if (StrUtil.isNotBlank(deviceTypeString)) { + Integer type = Integer.valueOf(deviceTypeString); + if (AppDeviceType.isValid(type)) { + deviceType = type; + } + } + return deviceType; + } + + /** + * 获取请求头中的设备信息。 + * + * @return 设备类型,具体值可参考AppDeviceType常量类。 + */ + public static String getDeviceTypeWithString() { + // 缺省都按照Web登录方式设置,如果前端header中的值为不合法值,这里也不会报错,而是使用Web缺省方式。 + int deviceType = AppDeviceType.WEB; + String deviceTypeString = ContextUtil.getHttpRequest().getHeader("deviceType"); + if (StrUtil.isNotBlank(deviceTypeString)) { + Integer type = Integer.valueOf(deviceTypeString); + if (AppDeviceType.isValid(type)) { + deviceType = type; + } + } + return AppDeviceType.getDeviceTypeName(deviceType); + } + + /** + * 获取第三方应用的编码。 + * + * @return 第三方应用编码。 + */ + public static String getAppCodeFromRequest() { + HttpServletRequest request = ContextUtil.getHttpRequest(); + String appCode = request.getHeader("AppCode"); + if (StrUtil.isBlank(appCode)) { + appCode = request.getParameter("AppCode"); + } + return appCode; + } + + /** + * 获取用户身份令牌。 + * + * @param tokenKey 令牌的Key。 + * @return 用户身份令牌。 + */ + public static String getTokenFromRequest(String tokenKey) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + String token = request.getHeader(tokenKey); + if (StrUtil.isBlank(token)) { + token = request.getParameter(tokenKey); + } + if (StrUtil.isBlank(token)) { + token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN); + } + return token; + } + + /** + * 转换为字典格式的数据列表。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, Function idGetter, Function nameGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 转换为树形字典格式的数据列表。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param parentIdGetter 获取字典Id父字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, + Function idGetter, + Function nameGetter, + Function parentIdGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + dataMap.put(ApplicationConstant.PARENT_ID, parentIdGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 转换为字典格式的数据列表,同时支持一个附加字段。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param extraName 附加字段名。。 + * @param extraGetter 获取附加字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @param 附加字段值的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, + Function idGetter, + Function nameGetter, + String extraName, + Function extraGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + dataMap.put(extraName, extraGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 将SQL查询条件中的变量值替换为SQL拼接的字符串值。 + * + * @param value 参数值。 + * @return 转换后的参数字符串。 + */ + public static String convertSqlParamValue(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof Number) { + return String.valueOf(value); + } + if (value instanceof Boolean) { + return String.valueOf(value.equals(Boolean.TRUE) ? 1 : 0); + } + StringBuilder builder = new StringBuilder(); + builder.append("'"); + if (value instanceof Date) { + builder.append(DateUtil.format((Date) value, MyDateUtil.COMMON_SHORT_DATETIME_FORMAT)); + } else if (value instanceof String) { + builder.append(value); + } + builder.append("'"); + return builder.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyCommonUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java new file mode 100644 index 00000000..3f4c2c1a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java @@ -0,0 +1,23 @@ +package com.orangeforms.common.core.util; + +import org.springframework.stereotype.Component; + +/** + * 缺省的自定义脱敏处理器的实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class MyCustomMaskFieldHandler implements MaskFieldHandler { + + @Override + public String handleMask(String modelName, String fieldName, String data, char maskChar) { + // 这里是我们默认提供的躺平实现方式。 + // 在默认生成的代码中,如果脱敏字段的处理类型为CUSTOM的时候,就会暂时使用 + // 该类为默认实现,其实这里就是一个占位符实现类。用户可根据需求自行实现自己所需的脱敏处理器实现类。 + // 实现后,可在脱敏字段的MaskField注解的handler参数中,改为自己的实现类。 + // 最后一句很重要,实现类必须是bean对象,如当前类用@Component注解标记。 + throw new UnsupportedOperationException("请仔细阅读上面的代码注解,并实现自己的处理类,以替代默认生成的自定义实现类!!"); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java new file mode 100644 index 00000000..033c5178 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java @@ -0,0 +1,320 @@ +package com.orangeforms.common.core.util; + +import com.orangeforms.common.core.object.Tuple2; +import org.apache.commons.lang3.time.DateUtils; +import org.joda.time.DateTime; +import org.joda.time.Period; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import java.util.Calendar; +import java.util.Date; + +import static org.joda.time.PeriodType.days; + +/** + * 日期工具类,主要封装了部分joda-time中的方法,让很多代码一行完成,同时统一了日期到字符串的pattern格式。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyDateUtil { + + /** + * 统一的日期pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_DATE_FORMAT = "yyyy-MM-dd"; + /** + * 统一的日期时间pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + /** + * 统一的短日期时间pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_SHORT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + /** + * 缺省日期格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATE_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_DATE_FORMAT); + /** + * 缺省日期时间格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATETIME_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_DATETIME_FORMAT); + + /** + * 缺省短日期时间格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATETIME_SHORT_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + + /** + * 获取一天的开始时间的字符串格式,如2019-08-03 00:00:00.000。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginTimeOfDay(DateTime dateTime) { + return dateTime.withTimeAtStartOfDay().toString(COMMON_DATETIME_FORMAT); + } + + /** + * 获取一天的结束时间的字符串格式,如2019-08-03 23:59:59.999。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndTimeOfDay(DateTime dateTime) { + return dateTime.withTime(23, 59, 59, 999).toString(COMMON_DATETIME_FORMAT); + } + + /** + * 获取一天的开始时间的字符串短格式,如2019-08-03 00:00:00。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginTimeOfDayWithShort(DateTime dateTime) { + return dateTime.withTimeAtStartOfDay().toString(COMMON_SHORT_DATETIME_FORMAT); + } + + /** + * 获取一天的结束时间的字符串短格式,如2019-08-03 23:59:59。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndTimeOfDayWithShort(DateTime dateTime) { + return dateTime.withTime(23, 59, 59, 999).toString(COMMON_SHORT_DATETIME_FORMAT); + } + + /** + * 获取参数时间对象所在周的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfWeek(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfWeek().withMinimumValue()); + } + + /** + * 获取参数时间对象所在周的结束时间的字符串短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfWeek(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfWeek().withMaximumValue()); + } + + /** + * 获取参数时间对象所在月份第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfMonth(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfMonth().withMinimumValue()); + } + + /** + * 获取参数时间对象所在月份的结束时间的字符串短格式, + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfMonth(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfMonth().withMaximumValue()); + } + + /** + * 获取参数时间对象所在年的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfYear(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfYear().withMinimumValue()); + } + + /** + * 获取参数时间对象所在年的结束时间的字符串短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfYear(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfYear().withMaximumValue()); + } + + + /** + * 获取参数时间对象所在季度的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfQuarter(DateTime dateTime) { + int m = dateTime.getMonthOfYear(); + int m2 = 10; + if (m >= 1 && m <= 3) { + m2 = 1; + } else if (m >= 4 && m <= 6) { + m2 = 4; + } else if (m >= 7 && m <= 9) { + m2 = 7; + } + return getBeginTimeOfDayWithShort(dateTime.withMonthOfYear(m2).dayOfMonth().withMinimumValue()); + } + + /** + * 获取参数时间对象所在季度的结束时间的字符串短格式, + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfQuarter(DateTime dateTime) { + int m = dateTime.getMonthOfYear(); + int m2 = 12; + if (m >= 1 && m <= 3) { + m2 = 3; + } else if (m >= 4 && m <= 6) { + m2 = 6; + } else if (m >= 7 && m <= 9) { + m2 = 9; + } + return getEndTimeOfDayWithShort(dateTime.withMonthOfYear(m2).dayOfMonth().withMaximumValue()); + } + + /** + * 获取一天中的开始时间和结束时间的字符串格式,如2019-08-03 00:00:00.000 和 2019-08-03 23:59:59.999。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 包含格式后字符串的二元组对象。 + */ + public static Tuple2 getDateTimeRangeOfDay(DateTime dateTime) { + return new Tuple2<>(getBeginTimeOfDay(dateTime), getEndTimeOfDay(dateTime)); + } + + /** + * 获取本月第一天的日期格式。如2019-08-01。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateOfMonth(DateTime dateTime) { + return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT); + } + + /** + * 获取本月第一天的日期格式。如2019-08-01。 + * + * @param dateString 待格式化的日期字符串对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateOfMonth(String dateString) { + DateTime dateTime = toDate(dateString); + return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT); + } + + /** + * 计算指定日期距离今天相差的天数。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 相差天数。 + */ + public static int getDayDiffToNow(DateTime dateTime) { + return new Period(dateTime, new DateTime(), days()).getDays(); + } + + /** + * 将日期对象格式化为缺省的字符串格式。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String toDateString(DateTime dateTime) { + return dateTime.toString(COMMON_DATE_FORMAT); + } + + /** + * 将日期时间对象格式化为缺省的字符串格式。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String toDateTimeString(DateTime dateTime) { + return dateTime.toString(COMMON_DATETIME_FORMAT); + } + + /** + * 将缺省格式的日期字符串解析为日期对象。 + * + * @param dateString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDate(String dateString) { + return DATE_PARSE_FORMATTER.parseDateTime(dateString); + } + + /** + * 将缺省格式的日期字符串解析为日期对象。 + * + * @param dateTimeString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDateTime(String dateTimeString) { + return DATETIME_PARSE_FORMATTER.parseDateTime(dateTimeString); + } + + /** + * 将缺省格式的(不包含毫秒的)日期时间字符串解析为日期对象。 + * + * @param dateTimeString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDateTimeWithoutMs(String dateTimeString) { + return DATETIME_SHORT_PARSE_FORMATTER.parseDateTime(dateTimeString); + } + + /** + * 截取时间到天。如2019-10-03 01:20:30 转换为 2019-10-03 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToDay(Date date) { + return DateUtils.truncate(date, Calendar.DAY_OF_MONTH); + } + + /** + * 截取时间到月。如2019-10-03 01:20:30 转换为 2019-10-01 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToMonth(Date date) { + return DateUtils.truncate(date, Calendar.MONTH); + } + + /** + * 截取时间到年。如2019-10-03 01:20:30 转换为 2019-01-01 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToYear(Date date) { + return DateUtils.truncate(date, Calendar.YEAR); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyDateUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java new file mode 100644 index 00000000..0d80d772 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java @@ -0,0 +1,875 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreInfo; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 负责Model数据操作、类型转换和关系关联等行为的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MyModelUtil { + + /** + * 数值型字段。 + */ + public static final Integer NUMERIC_FIELD_TYPE = 0; + /** + * 字符型字段。 + */ + public static final Integer STRING_FIELD_TYPE = 1; + /** + * 日期型字段。 + */ + public static final Integer DATE_FIELD_TYPE = 2; + /** + * 整个工程的实体对象中,创建者Id字段的Java对象名。 + */ + public static final String CREATE_USER_ID_FIELD_NAME = "createUserId"; + /** + * 整个工程的实体对象中,创建时间字段的Java对象名。 + */ + public static final String CREATE_TIME_FIELD_NAME = "createTime"; + /** + * 整个工程的实体对象中,更新者Id字段的Java对象名。 + */ + public static final String UPDATE_USER_ID_FIELD_NAME = "updateUserId"; + /** + * 整个工程的实体对象中,更新时间字段的Java对象名。 + */ + public static final String UPDATE_TIME_FIELD_NAME = "updateTime"; + /** + * mapToColumnName和mapToColumnInfo使用的缓存。 + */ + private static final Map> CACHED_COLUMNINFO_MAP = new ConcurrentHashMap<>(); + + /** + * 将Bean转换为Map。 + * + * @param data Bean数据对象。 + * @param Bean对象类型。 + * @return 转换后的Map。 + */ + public static Map beanToMap(T data) { + return BeanUtil.beanToMap(data); + } + + /** + * 将Bean的数据列表转换为Map列表。 + * + * @param dataList Bean数据列表。 + * @param Bean对象类型。 + * @return 转换后的Map列表。 + */ + public static List> beanToMapList(List dataList) { + return CollUtil.isEmpty(dataList) ? new LinkedList<>() + : dataList.stream().map(BeanUtil::beanToMap).collect(Collectors.toList()); + } + + /** + * 将Map的数据列表转换为Bean列表。 + * + * @param dataList Map数据列表。 + * @param Bean对象类型。 + * @return 转换后的Bean对象列表。 + */ + public static List mapToBeanList(List> dataList, Class clazz) { + return CollUtil.isEmpty(dataList) ? new LinkedList<>() + : dataList.stream().map(data -> BeanUtil.toBeanIgnoreError(data, clazz)).collect(Collectors.toList()); + } + + /** + * 拷贝源类型的集合数据到目标类型的集合中,其中源类型和目标类型中的对象字段类型完全相同。 + * NOTE: 该函数主要应用于框架中,Dto和Model之间的copy,特别针对一对一关联的深度copy。 + * 在Dto中,一对一对象可以使用Map来表示,而不需要使用从表对象的Dto。 + * + * @param sourceCollection 源类型集合。 + * @param targetClazz 目标类型的Class对象。 + * @param 源类型。 + * @param 目标类型。 + * @return copy后的目标类型对象集合。 + */ + public static List copyCollectionTo(Collection sourceCollection, Class targetClazz) { + List targetList = null; + if (sourceCollection == null) { + return targetList; + } + targetList = new LinkedList<>(); + if (CollUtil.isNotEmpty(sourceCollection)) { + for (S source : sourceCollection) { + try { + T target = targetClazz.newInstance(); + BeanUtil.copyProperties(source, target); + targetList.add(target); + } catch (Exception e) { + log.error("Failed to call MyModelUtil.copyCollectionTo", e); + return Collections.emptyList(); + } + } + } + return targetList; + } + + /** + * 拷贝源类型的对象数据到目标类型的对象中,其中源类型和目标类型中的对象字段类型完全相同。 + * NOTE: 该函数主要应用于框架中,Dto和Model之间的copy,特别针对一对一关联的深度copy。 + * 在Dto中,一对一对象可以使用Map来表示,而不需要使用从表对象的Dto。 + * + * @param source 源类型对象。 + * @param targetClazz 目标类型的Class对象。 + * @param 源类型。 + * @param 目标类型。 + * @return copy后的目标类型对象。 + */ + public static T copyTo(S source, Class targetClazz) { + if (source == null) { + return null; + } + try { + T target = targetClazz.newInstance(); + BeanUtil.copyProperties(source, target); + return target; + } catch (Exception e) { + log.error("Failed to call MyModelUtil.copyTo", e); + return null; + } + } + + /** + * 映射Model对象的字段反射对象,获取与该字段对应的数据库列名称。 + * + * @param field 字段反射对象。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String mapToColumnName(Field field, Class modelClazz) { + return mapToColumnName(field.getName(), modelClazz); + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String mapToColumnName(String fieldName, Class modelClazz) { + Tuple2 columnInfo = mapToColumnInfo(fieldName, modelClazz); + return columnInfo == null ? null : columnInfo.getFirst(); + } + + /** + * 映射Model对象的字段反射对象,获取与该字段对应的数据库列名称。 + * 如果没有匹配到ColumnName,则立刻抛出异常。 + * + * @param field 字段反射对象。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String safeMapToColumnName(Field field, Class modelClazz) { + return safeMapToColumnName(field.getName(), modelClazz); + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称。 + * 如果没有匹配到ColumnName,则立刻抛出异常。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String safeMapToColumnName(String fieldName, Class modelClazz) { + String columnName = mapToColumnName(fieldName, modelClazz); + if (columnName == null) { + throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName); + } + return columnName; + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称和字段类型。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称和Java字段类型。 + */ + public static Tuple2 mapToColumnInfo(String fieldName, Class modelClazz) { + if (StrUtil.isBlank(fieldName)) { + return null; + } + StringBuilder sb = new StringBuilder(128); + sb.append(modelClazz.getName()).append("-#-").append(fieldName); + Tuple2 columnInfo = CACHED_COLUMNINFO_MAP.get(sb.toString()); + if (columnInfo != null) { + return columnInfo; + } + Field field = ReflectUtil.getField(modelClazz, fieldName); + if (field == null) { + return null; + } + Column c = field.getAnnotation(Column.class); + String columnName = null; + if (c == null) { + Id id = field.getAnnotation(Id.class); + if (id != null) { + columnName = id.value(); + } + } + if (StrUtil.isBlank(columnName)) { + columnName = c == null ? CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName) : c.value(); + if (StrUtil.isBlank(columnName)) { + columnName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName); + } + } + // 这里缺省情况下都是按照整型去处理,因为他覆盖太多的类型了。 + // 如Integer/Long/Double/BigDecimal,可根据实际情况完善和扩充。 + String typeName = field.getType().getSimpleName(); + Integer type = NUMERIC_FIELD_TYPE; + if (String.class.getSimpleName().equals(typeName)) { + type = STRING_FIELD_TYPE; + } else if (Date.class.getSimpleName().equals(typeName)) { + type = DATE_FIELD_TYPE; + } + columnInfo = new Tuple2<>(columnName, type); + CACHED_COLUMNINFO_MAP.put(sb.toString(), columnInfo); + return columnInfo; + } + + /** + * 映射Model主对象的Class名称,到Model所对应的表名称。 + * + * @param modelClazz Model主对象的Class。 + * @return Model对象对应的数据表名称。 + */ + public static String mapToTableName(Class modelClazz) { + Table t = modelClazz.getAnnotation(Table.class); + return t == null ? null : t.value(); + } + + /** + * 主Model类型中,遍历所有包含RelationConstDict注解的字段,并将关联的静态字典中的数据, + * 填充到thisModel对象的被注解字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModel 主对象。 + * @param 主表对象类型。 + */ + @SuppressWarnings("unchecked") + public static void makeConstDictRelation(Class thisClazz, T thisModel) { + if (thisModel == null) { + return; + } + Field[] fields = ReflectUtil.getFields(thisClazz); + for (Field field : fields) { + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName()); + RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class); + if (r == null) { + continue; + } + Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = + (Map) ReflectUtil.getFieldValue(r.constantDictClass(), dictMapField); + Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField()); + if (id != null) { + String name = dictMap.get(id); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + } + + /** + * 主Model类型中,遍历所有包含RelationConstDict注解的字段,并将关联的静态字典中的数据, + * 填充到thisModelList集合元素对象的被注解字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param 主表对象类型。 + */ + @SuppressWarnings("unchecked") + public static void makeConstDictRelation(Class thisClazz, List thisModelList) { + if (CollUtil.isEmpty(thisModelList)) { + return; + } + List thisModelList2 = thisModelList.stream().filter(Objects::nonNull).toList(); + Field[] fields = ReflectUtil.getFields(thisClazz); + for (Field field : fields) { + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName()); + RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class); + if (r == null) { + continue; + } + Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = + (Map) ReflectUtil.getFieldValue(r.constantDictClass(), dictMapField); + for (T thisModel : thisModelList2) { + Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField()); + if (id != null) { + String name = dictMap.get(id); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + } + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象thatModel中的数据, + * 关联到thisModel对象的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModel 主对象。 + * @param thatModel 字典关联对象。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, T thisModel, R thatModel, String thisRelationField) { + if (thatModel == null || thisModel == null) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Object slaveId = ReflectUtil.getFieldValue(thatModel, r.slaveIdField()); + if (slaveId != null) { + Map m = new HashMap<>(2); + m.put("id", slaveId); + m.put("name", ReflectUtil.getFieldValue(thatModel, r.slaveNameField())); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象集合thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 字典关联对象列表集合。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField()); + Map thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.put(id, thatModel); + } + }); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMap.get(id); + if (thatModel != null) { + Map m = new HashMap<>(4); + m.put("id", id); + m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField)); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象集合thatModelMap中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * 该函数之所以使用Map,主要出于性能优化考虑,在连续使用thatModelMap进行关联时,有效的避免了从多次从List转换到Map的过程。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatMadelMap 字典关联对象映射集合。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, List thisModelList, Map thatMadelMap, String thisRelationField) { + if (MapUtil.isEmpty(thatMadelMap) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField()); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMadelMap.get(id); + if (thatModel != null) { + Map m = new HashMap<>(4); + m.put("id", id); + m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField)); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationGlobalDict注解参数,全局字典dictMap中的字典数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param dictMap 全局字典数据。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + */ + public static void makeGlobalDictRelation( + Class thisClazz, List thisModelList, Map dictMap, String thisRelationField) { + if (MapUtil.isEmpty(dictMap) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationGlobalDict r = thisTargetField.getAnnotation(RelationGlobalDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + String name = dictMap.get(id.toString()); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationOneToOne注解参数,将被关联对象列表thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 一对一关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationOneToOne r = thisTargetField.getAnnotation(RelationOneToOne.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Map thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.put(id, thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMap.get(id); + if (thatModel != null) { + if (thisTargetField.getType().equals(Map.class)) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, BeanUtil.beanToMap(thatModel)); + } else { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + } + }); + } + + /** + * 根据主对象和关联对象各自的关联Id函数,将主对象列表和关联对象列表中的数据关联到一起,并将关联对象 + * 设置到主对象的指定关联字段中。 + * NOTE: 用于主对象关联字段中,没有包含RelationOneToOne注解的场景。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thisIdGetterFunc 主对象Id的Getter函数。 + * @param thatModelList 关联对象列表。 + * @param thatIdGetterFunc 关联对象Id的Getter函数。 + * @param thisRelationField 主对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, + List thisModelList, + Function thisIdGetterFunc, + List thatModelList, + Function thatIdGetterFunc, + String thisRelationField) { + makeOneToOneRelation(thisClazz, thisModelList, + thisIdGetterFunc, thatModelList, thatIdGetterFunc, thisRelationField, false); + } + + /** + * 根据主对象和关联对象各自的关联Id函数,将主对象列表和关联对象列表中的数据关联到一起,并将关联对象 + * 设置到主对象的指定关联字段中。 + * NOTE: 用于主对象关联字段中,没有包含RelationOneToOne注解的场景。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thisIdGetterFunc 主对象Id的Getter函数。 + * @param thatModelList 关联对象列表。 + * @param thatIdGetterFunc 关联对象Id的Getter函数。 + * @param thisRelationField 主对象中保存被关联对象的字段名称。 + * @param orderByThatList 如果为true,则按照ThatModelList的顺序输出。同时thisModelList被排序后的新列表替换。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, + List thisModelList, + Function thisIdGetterFunc, + List thatModelList, + Function thatIdGetterFunc, + String thisRelationField, + boolean orderByThatList) { + if (CollUtil.isEmpty(thisModelList)) { + return; + } + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + boolean isMap = thisTargetField.getType().equals(Map.class); + if (orderByThatList) { + List newThisModelList = new LinkedList<>(); + Map thisModelMap = + thisModelList.stream().collect(Collectors.toMap(thisIdGetterFunc, c -> c)); + thatModelList.forEach(thatModel -> { + Object thatId = thatIdGetterFunc.apply(thatModel); + if (thatId != null) { + T thisModel = thisModelMap.get(thatId); + if (thisModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel)); + newThisModelList.add(thisModel); + } + } + }); + thisModelList.clear(); + thisModelList.addAll(newThisModelList); + return; + } + Map thatMadelMap = + thatModelList.stream().collect(Collectors.toMap(thatIdGetterFunc, c -> c)); + thisModelList.forEach(thisModel -> { + Object thisId = thisIdGetterFunc.apply(thisModel); + if (thisId != null) { + R thatModel = thatMadelMap.get(thisId); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel)); + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationOneToMany注解参数,将被关联对象列表thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 一对多关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToManyRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationOneToMany r = thisTargetField.getAnnotation(RelationOneToMany.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Map> thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + List thatModelSubList = thatMap.computeIfAbsent(id, k -> new LinkedList<>()); + thatModelSubList.add(thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + List thatModel = thatMap.get(id); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationManyToMany注解参数,将被关联对象列表relationModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param idFieldName 主表主键Id字段名。 + * @param thisModelList 主对象列表。 + * @param relationModelList 多对多关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 关联表对象类型。 + */ + public static void makeManyToManyRelation( + Class thisClazz, String idFieldName, List thisModelList, List relationModelList, String thisRelationField) { + if (CollUtil.isEmpty(relationModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationManyToMany r = thisTargetField.getAnnotation(RelationManyToMany.class); + Field masterIdField = ReflectUtil.getField(thisClazz, idFieldName); + Class thatClass = r.relationModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.relationMasterIdField()); + Map> thatMap = new HashMap<>(20); + relationModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.computeIfAbsent(id, k -> new LinkedList<>()).add(thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + List thatModel = thatMap.get(id); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + }); + } + + private static Object normalize(boolean isMap, M model) { + return isMap ? BeanUtil.beanToMap(model) : model; + } + + /** + * 获取上传字段的存储信息。 + * + * @param modelClass model的class对象。 + * @param uploadFieldName 上传字段名。 + * @param model的类型。 + * @return 字段的上传存储信息对象。该值始终不会返回null。 + */ + public static UploadStoreInfo getUploadStoreInfo(Class modelClass, String uploadFieldName) { + UploadStoreInfo uploadStoreInfo = new UploadStoreInfo(); + Field uploadField = ReflectUtil.getField(modelClass, uploadFieldName); + if (uploadField == null) { + throw new UnsupportedOperationException("The Field [" + + uploadFieldName + "] doesn't exist in Model [" + modelClass.getSimpleName() + "]."); + } + uploadStoreInfo.setSupportUpload(false); + UploadFlagColumn anno = uploadField.getAnnotation(UploadFlagColumn.class); + if (anno != null) { + uploadStoreInfo.setSupportUpload(true); + uploadStoreInfo.setStoreType(anno.storeType()); + } + return uploadStoreInfo; + } + + /** + * 在插入实体对象数据之前,可以调用该方法,初始化通用字段的数据。 + * + * @param data 实体对象。 + * @param 实体对象类型。 + */ + public static void fillCommonsForInsert(M data) { + Field createdByField = ReflectUtil.getField(data.getClass(), CREATE_USER_ID_FIELD_NAME); + if (createdByField != null) { + ReflectUtil.setFieldValue(data, createdByField, TokenData.takeFromRequest().getUserId()); + } + Field createTimeField = ReflectUtil.getField(data.getClass(), CREATE_TIME_FIELD_NAME); + if (createTimeField != null) { + ReflectUtil.setFieldValue(data, createTimeField, new Date()); + } + Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME); + if (updatedByField != null) { + ReflectUtil.setFieldValue(data, updatedByField, TokenData.takeFromRequest().getUserId()); + } + Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME); + if (updateTimeField != null) { + ReflectUtil.setFieldValue(data, updateTimeField, new Date()); + } + } + + /** + * 在更新实体对象数据之前,可以调用该方法,更新通用字段的数据。 + * + * @param data 实体对象。 + * @param originalData 原有实体对象。 + * @param 实体对象类型。 + */ + public static void fillCommonsForUpdate(M data, M originalData) { + Object createdByValue = ReflectUtil.getFieldValue(originalData, CREATE_USER_ID_FIELD_NAME); + if (createdByValue != null) { + ReflectUtil.setFieldValue(data, CREATE_USER_ID_FIELD_NAME, createdByValue); + } + Object createTimeValue = ReflectUtil.getFieldValue(originalData, CREATE_TIME_FIELD_NAME); + if (createTimeValue != null) { + ReflectUtil.setFieldValue(data, CREATE_TIME_FIELD_NAME, createTimeValue); + } + Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME); + if (updatedByField != null) { + ReflectUtil.setFieldValue(data, updatedByField, TokenData.takeFromRequest().getUserId()); + } + Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME); + if (updateTimeField != null) { + ReflectUtil.setFieldValue(data, updateTimeField, new Date()); + } + } + + /** + * 为实体对象字段设置缺省值。如果data对象中指定字段的值为NULL,则设置缺省值,否则跳过。 + * + * @param data 实体对象。 + * @param fieldName 实体对象字段名。 + * @param defaultValue 缺省值。 + * @param 实体对象类型。 + * @param 缺省值类型。 + */ + public static void setDefaultValue(M data, String fieldName, V defaultValue) { + Object v = ReflectUtil.getFieldValue(data, fieldName); + if (v == null) { + ReflectUtil.setFieldValue(data, fieldName, defaultValue); + } + } + + /** + * 获取当前数据对象中,所有上传文件字段的数据,并将上传后的文件名存到集合中并返回。 + * + * @param data 数据对象。 + * @param clazz 数据对象的Class类型。 + * @param 数据对象类型。 + * @return 当前数据对象中,所有上传文件字段中,文件名属性的集合。 + */ + public static Set extractDownloadFileName(M data, Class clazz) { + Set resultSet = new HashSet<>(); + if (data == null) { + return resultSet; + } + Field[] fields = ReflectUtil.getFields(clazz); + for (Field field : fields) { + if (field.isAnnotationPresent(UploadFlagColumn.class)) { + String v = (String) ReflectUtil.getFieldValue(data, field); + List fileInfoList = JSON.parseArray(v, UploadResponseInfo.class); + if (CollUtil.isNotEmpty(fileInfoList)) { + fileInfoList.forEach(fileInfo -> resultSet.add(fileInfo.getFilename())); + } + } + } + return resultSet; + } + + /** + * 获取当前数据对象列表中,所有上传文件字段的数据,并将上传后的文件名存到集合中并返回。 + * + * @param dataList 数据对象。 + * @param clazz 数据对象的Class类型。 + * @param 数据对象类型。 + * @return 当前数据对象中,所有上传文件字段中,文件名属性的集合。 + */ + public static Set extractDownloadFileName(List dataList, Class clazz) { + Set resultSet = new HashSet<>(); + if (CollUtil.isEmpty(dataList)) { + return resultSet; + } + dataList.forEach(data -> resultSet.addAll(extractDownloadFileName(data, clazz))); + return resultSet; + } + + /** + * 根据数据对象指定字段的类型,将参数中的字段值集合转换为匹配的值类型集合。 + * @param clazz 数据对象的Class。 + * @param fieldName 字段名。 + * @param fieldValues 字符型的字段值集合。 + * @param 对象类型。 + * @return 转换后的字段值集合。 + */ + public static Set convertToTypeValues( + Class clazz, String fieldName, List fieldValues) { + Field f = ReflectUtil.getField(clazz, fieldName); + if (f == null) { + String errorMsg = "数据对象 [" + clazz.getSimpleName() + " ] 中,不存在该数据字段 [" + fieldName + "]!"; + throw new MyRuntimeException(errorMsg); + } + if (f.getType().equals(Long.class)) { + return fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet()); + } else if (f.getType().equals(Integer.class)) { + return fieldValues.stream().map(Integer::valueOf).collect(Collectors.toSet()); + } + return new HashSet<>(fieldValues); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyModelUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java new file mode 100644 index 00000000..fc2c7d8f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java @@ -0,0 +1,155 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.jimmyshi.beanquery.BeanQuery; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.Page; +import org.apache.commons.collections4.CollectionUtils; +import com.orangeforms.common.core.base.mapper.BaseModelMapper; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.Tuple2; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 生成带有分页信息的数据列表 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyPageUtil { + + private static final String DATA_LIST_LITERAL = "dataList"; + private static final String TOTAL_COUNT_LITERAL = "totalCount"; + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param includeFields 结果集中需要返回到前端的字段,多个字段之间逗号分隔。 + * @return 返回只是包含includeFields字段的数据列表,以及结果集TotalCount。 + */ + public static JSONObject makeResponseData(List dataList, String includeFields) { + JSONObject pageData = new JSONObject(); + pageData.put(DATA_LIST_LITERAL, BeanQuery.select(includeFields).from(dataList).execute()); + if (dataList instanceof Page) { + pageData.put(TOTAL_COUNT_LITERAL, ((Page)dataList).getTotal()); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList) { + MyPageData pageData = new MyPageData<>(); + pageData.setDataList(dataList); + if (dataList instanceof Page) { + pageData.setTotalCount(((Page)dataList).getTotal()); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param totalCount 总数量。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Long totalCount) { + MyPageData pageData = new MyPageData<>(); + pageData.setDataList(dataList); + if (totalCount != null) { + pageData.setTotalCount(totalCount); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param modelMapper 实体对象到DomainVO对象的数据映射器。 + * @param DomainVO对象类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, BaseModelMapper modelMapper) { + long totalCount = 0L; + if (CollectionUtils.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + return MyPageUtil.makeResponseData(modelMapper.fromModelList(dataList), totalCount); + } + + /** + * 构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param converter 转换函数对象。 + * @param 结果类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Function converter) { + long totalCount = 0L; + if (CollUtil.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + List resultList = dataList.stream().map(converter).collect(Collectors.toList()); + return MyPageUtil.makeResponseData(resultList, totalCount); + } + + /** + * 构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param targetClazz 模板对象类型。 + * @param 结果类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Class targetClazz) { + long totalCount = 0L; + if (CollUtil.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + List resultList = MyModelUtil.copyCollectionTo(dataList, targetClazz); + return MyPageUtil.makeResponseData(resultList, totalCount); + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param responseData 第一个数据时数据列表,第二个是列表数量。 + * @param 源数据类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(Tuple2, Long> responseData) { + return makeResponseData(responseData.getFirst(), responseData.getSecond()); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyPageUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java new file mode 100644 index 00000000..23494356 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java @@ -0,0 +1,187 @@ +package com.orangeforms.common.core.util; + +import com.orangeforms.common.core.object.TokenData; + +/** + * Redis 键生成工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class RedisKeyUtil { + + private static final String SESSIONID_PREFIX = "SESSIONID:"; + + /** + * 获取通用的session缓存的键前缀。 + * + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix() { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_"; + } + + /** + * 获取指定用户Id的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(String loginName) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_"; + } + + /** + * 获取指定用户Id的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @param tokenData 令牌对象。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(TokenData tokenData, String loginName) { + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_"; + } + + /** + * 获取指定用户Id和登录设备类型的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @param deviceType 设备类型。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(String loginName, int deviceType) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_" + deviceType + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_" + deviceType + "_"; + } + + /** + * 计算SessionId返回存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话存储于Redis中的键值。 + */ + public static String makeSessionIdKey(String sessionId) { + return SESSIONID_PREFIX + sessionId; + } + + /** + * 计算SessionId关联的权限数据存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的权限数据存储于Redis中的键值。 + */ + public static String makeSessionPermIdKey(String sessionId) { + return "PERM:" + sessionId; + } + + /** + * 计算SessionId关联的权限字存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的权限字存储于Redis中的键值。 + */ + public static String makeSessionPermCodeKey(String sessionId) { + return "PERM_CODE:" + sessionId; + } + + /** + * 计算SessionId关联的数据权限数据存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的数据权限数据存储于Redis中的键值。 + */ + public static String makeSessionDataPermIdKey(String sessionId) { + return "DATA_PERM:" + sessionId; + } + + /** + * 计算包含全局字典及其数据项的缓存键。 + * + * @param dictCode 全局字典编码。 + * @return 全局字典指定编码的缓存键。 + */ + public static String makeGlobalDictKey(String dictCode) { + return "GLOBAL_DICT:" + dictCode; + } + + /** + * 计算仅仅包含全局字典对象数据的缓存键。 + * + * @param dictCode 全局字典编码。 + * @return 全局字典指定编码的缓存键。 + */ + public static String makeGlobalDictOnlyKey(String dictCode) { + return "GLOBAL_DICT_ONLY:" + dictCode; + } + + /** + * 计算会话的菜单Id关联权限资源URL的缓存键。 + * + * @param sessionId 会话Id。 + * @param menuId 菜单Id。 + * @return 计算后的缓存键。 + */ + public static String makeSessionMenuPermKey(String sessionId, Object menuId) { + return "SESSION_MENU_ID:" + sessionId + "-" + menuId.toString(); + } + + /** + * 计算会话的菜单Id关联权限资源URL的缓存键的前缀。 + * + * @param sessionId 会话Id。 + * @return 计算后的缓存键前缀。 + */ + public static String getSessionMenuPermPrefix(String sessionId) { + return "SESSION_MENU_ID:" + sessionId + "-"; + } + + /** + * 计算会话关联的白名单URL的缓存键。 + * + * @param sessionId 会话Id。 + * @return 计算后的缓存键。 + */ + public static String makeSessionWhiteListPermKey(String sessionId) { + return "SESSION_WHITE_LIST:" + sessionId; + } + + /** + * 计算会话关联指定部门Ids的子部门Ids的缓存键。 + * + * @param sessionId 会话Id。 + * @param deptIds 部门Id,多个部门Id之间逗号分割。 + * @return 计算后的缓存键。 + */ + public static String makeSessionChildrenDeptIdKey(String sessionId, String deptIds) { + return "SESSION_CHILDREN_DEPT_ID:" + sessionId + "-" + deptIds; + } + + /** + * 计算租户编码的缓存键。 + * + * @param tenantCode 租户编码。 + */ + public static String makeTenantCodeKey(String tenantCode) { + return "TENANT_CODE:" + tenantCode; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RedisKeyUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java new file mode 100644 index 00000000..05d34fb9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java @@ -0,0 +1,102 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import lombok.extern.slf4j.Slf4j; + +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +import java.util.Map; + +/** + * Java RSA 加密工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class RsaUtil { + + /** + * 密钥长度 于原文长度对应 以及越长速度越慢 + */ + private static final int KEY_SIZE = 1024; + /** + * 用于封装随机产生的公钥与私钥 + */ + private static final Map KEY_MAP = MapUtil.newHashMap(); + + /** + * 随机生成密钥对。 + */ + public static void genKeyPair() throws NoSuchAlgorithmException { + // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + // 初始化密钥对生成器 + keyPairGen.initialize(KEY_SIZE, new SecureRandom()); + // 生成一个密钥对,保存在keyPair中 + KeyPair keyPair = keyPairGen.generateKeyPair(); + // 得到私钥 + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // 得到公钥 + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded()); + // 得到私钥字符串 + String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded()); + // 将公钥和私钥保存到Map + // 0表示公钥 + KEY_MAP.put(0, publicKeyString); + // 1表示私钥 + KEY_MAP.put(1, privateKeyString); + } + + /** + * RSA公钥加密。 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + */ + public static String encrypt(String str, String publicKey) { + RSA rsa = new RSA(null, publicKey); + return Base64.getEncoder().encodeToString(rsa.encrypt(str, KeyType.PublicKey)); + } + + /** + * RSA私钥解密。 + * + * @param str 加密字符串 + * @param privateKey 私钥 + * @return 明文 + */ + public static String decrypt(String str, String privateKey) { + RSA rsa = new RSA(privateKey, null); + // 64位解码加密后的字符串 + return new String(rsa.decrypt(Base64.getDecoder().decode(str), KeyType.PrivateKey)); + } + + public static void main(String[] args) throws Exception { + long temp = System.currentTimeMillis(); + // 生成公钥和私钥 + genKeyPair(); + // 加密字符串 + log.info("公钥:" + KEY_MAP.get(0)); + log.info("私钥:" + KEY_MAP.get(1)); + log.info("生成密钥消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + log.info("生成后的公钥前端使用!"); + log.info("生成后的私钥后台使用!"); + String message = "RSA测试ABCD~!@#$"; + log.info("原文:" + message); + temp = System.currentTimeMillis(); + String messageEn = encrypt(message, KEY_MAP.get(0)); + log.info("密文:" + messageEn); + log.info("加密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + temp = System.currentTimeMillis(); + String messageDe = decrypt(messageEn, KEY_MAP.get(1)); + log.info("解密:" + messageDe); + log.info("解密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java new file mode 100644 index 00000000..5931410e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java @@ -0,0 +1,92 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 将列表结构组建为树结构的工具类。 + * + * @param 对象类型。 + * @param 节点之间关联键的类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class TreeNode { + + private K id; + private K parentId; + private T data; + private List> childList = new ArrayList<>(); + + /** + * 将列表结构组建为树结构的工具方法。 + * + * @param dataList 数据列表结构。 + * @param idFunc 获取关联id的函数对象。 + * @param parentIdFunc 获取关联ParentId的函数对象。 + * @param root 根节点。 + * @param 数据对象类型。 + * @param 节点之间关联键的类型。 + * @return 源数据对象的树结构存储。 + */ + public static List> build( + List dataList, Function idFunc, Function parentIdFunc, K root) { + List> treeNodeList = new ArrayList<>(); + for (T data : dataList) { + if (ObjectUtil.equals(parentIdFunc.apply(data), idFunc.apply(data))) { + continue; + } + TreeNode dataNode = new TreeNode<>(); + dataNode.setId(idFunc.apply(data)); + dataNode.setParentId(parentIdFunc.apply(data)); + dataNode.setData(data); + treeNodeList.add(dataNode); + } + return root == null ? toBuildTreeWithoutRoot(treeNodeList) : toBuildTree(treeNodeList, root); + } + + private static List> toBuildTreeWithoutRoot(List> treeNodes) { + Map> treeNodeMap = + treeNodes.stream().collect(Collectors.toMap(TreeNode::getId, n -> n)); + List> treeNodeList = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + TreeNode parentNode = treeNodeMap.get(treeNode.getParentId()); + if (parentNode == null) { + treeNodeList.add(treeNode); + } else { + parentNode.add(treeNode); + } + } + return treeNodeList; + } + + private static List> toBuildTree(List> treeNodes, K root) { + List> treeNodeList = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + if (root.equals(treeNode.getParentId())) { + treeNodeList.add(treeNode); + } + for (TreeNode it : treeNodes) { + if (it.getParentId() == treeNode.getId()) { + if (treeNode.getChildList() == null) { + treeNode.setChildList(new ArrayList<>()); + } + treeNode.add(it); + } + } + } + return treeNodeList; + } + + private void add(TreeNode node) { + childList.add(node); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java new file mode 100644 index 00000000..a287fd56 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java @@ -0,0 +1,10 @@ +package com.orangeforms.common.core.validator; + +/** + * 数据增加的验证分组。通常用于数据新增场景。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface AddGroup { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java new file mode 100644 index 00000000..00e43b6a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.core.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 定义在Model对象中,标注字段值引用自指定的常量字典,和ConstDictRefValidator对象配合完成数据验证。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ConstDictValidator.class) +public @interface ConstDictRef { + + /** + * 引用的常量字典对象,该对象必须包含isValid的静态方法。 + * + * @return 最大长度。 + */ + Class constDictClass(); + + /** + * 超过边界后的错误消息提示。 + * + * @return 错误提示。 + */ + String message() default "无效的字典引用值!"; + + /** + * 验证分组。 + * + * @return 验证分组。 + */ + Class[] groups() default {}; + + /** + * 载荷对象类型。 + * + * @return 载荷对象。 + */ + Class[] payload() default {}; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java new file mode 100644 index 00000000..ba58a2a7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.core.validator; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.lang.reflect.Method; + +/** + * * 数据字段自定义验证,用于验证Model中关联的常量字典值的合法性。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class ConstDictValidator implements ConstraintValidator { + + private ConstDictRef constDictRef; + + @Override + public void initialize(ConstDictRef constDictRef) { + this.constDictRef = constDictRef; + } + + @Override + public boolean isValid(Object s, ConstraintValidatorContext constraintValidatorContext) { + if (ObjectUtil.isEmpty(s)) { + return true; + } + Method method = + ReflectUtil.getMethodByName(constDictRef.constDictClass(), "isValid"); + return ReflectUtil.invokeStatic(method, s); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java new file mode 100644 index 00000000..c5a983fb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java @@ -0,0 +1,55 @@ +package com.orangeforms.common.core.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 定义在Model或Dto对象中,UTF-8编码的字符串字段长度的上限和下限,和TextLengthValidator对象配合完成数据验证。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = TextLengthValidator.class) +public @interface TextLength { + + /** + * 字符串字段的最小长度。 + * + * @return 最小长度。 + */ + int min() default 0; + + /** + * 字符串字段的最大长度。 + * + * @return 最大长度。 + */ + int max() default Integer.MAX_VALUE; + + /** + * 超过边界后的错误消息提示。 + * + * @return 错误提示。 + */ + String message() default "字段长度超过最大字节数!"; + + /** + * 验证分组。 + * + * @return 验证分组。 + */ + Class[] groups() default { }; + + /** + * 载荷对象类型。 + * + * @return 载荷对象。 + */ + Class[] payload() default { }; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java new file mode 100644 index 00000000..5433bc2b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.core.validator; + +import org.apache.commons.lang3.CharUtils; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * 数据字段自定义验证,用于验证Model中UTF-8编码的字符串字段的最大长度和最小长度。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class TextLengthValidator implements ConstraintValidator { + + private TextLength textLength; + + @Override + public void initialize(TextLength textLength) { + this.textLength = textLength; + } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (s == null) { + return true; + } + int length = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (CharUtils.isAscii(c)) { + ++length; + } else { + length += 2; + } + } + return length >= textLength.min() && length <= textLength.max(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java new file mode 100644 index 00000000..1c196a79 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java @@ -0,0 +1,11 @@ +package com.orangeforms.common.core.validator; + +/** + * 数据修改的验证分组。通常用于数据更新的场景。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface UpdateGroup { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/pom.xml new file mode 100644 index 00000000..e791d2f7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-datafilter + 1.0.0 + common-datafilter + jar + + + + com.orangeforms + common-core + 1.0.0 + + + com.orangeforms + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java new file mode 100644 index 00000000..91ab688d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java @@ -0,0 +1,42 @@ +package com.orangeforms.common.datafilter.aop; + +import com.orangeforms.common.core.object.GlobalThreadLocal; +import lombok.extern.slf4j.Slf4j; +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.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 禁用Mybatis拦截器数据过滤的AOP处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DisableDataFilterAspect { + + /** + * 所有标记了DisableDataFilter注解的类和方法。 + */ + @Pointcut("@within(com.orangeforms.common.core.annotation.DisableDataFilter) " + + "|| @annotation(com.orangeforms.common.core.annotation.DisableDataFilter)") + public void disableDataFilterPointCut() { + // 空注释,避免sonar警告 + } + + @Around("disableDataFilterPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + boolean dataFilterEnabled = GlobalThreadLocal.setDataFilter(false); + try { + return point.proceed(); + } finally { + GlobalThreadLocal.setDataFilter(dataFilterEnabled); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java new file mode 100644 index 00000000..eefef7b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.datafilter.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-datafilter模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({DataFilterProperties.class}) +public class DataFilterAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java new file mode 100644 index 00000000..f4019a9d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java @@ -0,0 +1,50 @@ +package com.orangeforms.common.datafilter.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common-datafilter模块的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-datafilter") +public class DataFilterProperties { + + /** + * 是否启用租户过滤。 + */ + @Value("${common-datafilter.tenant.enabled:false}") + private Boolean enabledTenantFilter; + + /** + * 是否启动数据权限过滤。 + */ + @Value("${common-datafilter.dataperm.enabled:false}") + private Boolean enabledDataPermFilter; + + /** + * 部门关联表的表名前缀,如zz_。该值主要用在MybatisDataFilterInterceptor拦截器中, + * 用于拼接数据权限过滤的SQL语句。 + */ + @Value("${common-datafilter.dataperm.deptRelationTablePrefix:}") + private String deptRelationTablePrefix; + + /** + * 该值为true的时候,在进行数据权限过滤时,会加上表名,如:zz_sys_user.dept_id = xxx。 + * 为false时,过滤条件不加表名,只是使用字段名,如:dept_id = xxx。该值目前主要适用于 + * Oracle分页SQL使用了子查询的场景。此场景下,由于子查询使用了别名,再在数据权限过滤条件中 + * 加上原有表名时,SQL语法会报错。 + */ + @Value("${common-datafilter.dataperm.addTableNamePrefix:true}") + private Boolean addTableNamePrefix; + + /** + * 是否打开menuId和当前url的匹配关系的验证。 + */ + @Value("${common-datafilter.dataperm.enableMenuPermVerify:true}") + private Boolean enableMenuPermVerify; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java new file mode 100644 index 00000000..2ba79d45 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.datafilter.config; + +import com.orangeforms.common.datafilter.interceptor.DataFilterInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 添加数据过滤相关的拦截器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class DataFilterWebMvcConfigurer implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new DataFilterInterceptor()).addPathPatterns("/**"); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java new file mode 100644 index 00000000..a20b9083 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java @@ -0,0 +1,42 @@ +package com.orangeforms.common.datafilter.interceptor; + +import com.orangeforms.common.core.object.GlobalThreadLocal; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 主要用于初始化,通过Mybatis拦截器插件进行数据过滤的标记。 + * 在调用controller接口处理方法之前,必须强制将数据过滤标记设置为缺省值。 + * 这样可以避免使用当前线程在处理上一个请求时,未能正常清理的数据过滤标记值。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class DataFilterInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + // 每次进入Controller接口之前,均主动打开数据权限验证。 + // 可以避免该Servlet线程在处理之前的请求时异常退出,从而导致该状态数据没有被正常清除。 + GlobalThreadLocal.setDataFilter(true); + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + // 这里需要加注释,否则sonar不happy。 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + GlobalThreadLocal.clearDataFilter(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java new file mode 100644 index 00000000..da29121b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java @@ -0,0 +1,646 @@ +package com.orangeforms.common.datafilter.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.mybatis.FlexStatementHandler; +import com.mybatisflex.core.mybatis.MapperInvocationHandler; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.exception.NoDataPermException; +import com.orangeforms.common.core.object.GlobalThreadLocal; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.core.constant.DataPermRuleType; +import com.orangeforms.common.datafilter.config.DataFilterProperties; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SubSelect; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.plugin.*; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.sql.Connection; +import java.util.*; + +/** + * Mybatis拦截器。目前用于数据权限的统一拦截和注入处理。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) +@Slf4j +@Component +public class MybatisDataFilterInterceptor implements Interceptor { + + @Autowired + private RedissonClient redissonClient; + @Autowired + private DataFilterProperties properties; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + /** + * 对象缓存。由于Set是排序后的,因此在查找排除方法名称时效率更高。 + * 在应用服务启动的监听器中(LoadDataPermMapperListener),会调用当前对象的(loadMappersWithDataPerm)方法,加载缓存。 + */ + private final Map cachedDataPermMap = MapUtil.newHashMap(); + /** + * 租户租户对象缓存。 + */ + private final Map cachedTenantMap = MapUtil.newHashMap(); + + /** + * 预先加载与数据过滤相关的数据到缓存,该函数会在(LoadDataFilterInfoListener)监听器中调用。 + */ + @SuppressWarnings("all") + public void loadInfoWithDataFilter() { + Map mapperMap = + ApplicationContextHolder.getApplicationContext().getBeansOfType(BaseDaoMapper.class); + for (BaseDaoMapper mapperProxy : mapperMap.values()) { + // 优先处理jdk的代理 + Object proxy = ReflectUtil.getFieldValue(mapperProxy, "h"); + // 如果不是jdk的代理,再看看cjlib的代理。 + if (proxy == null) { + proxy = ReflectUtil.getFieldValue(mapperProxy, "CGLIB$CALLBACK_0"); + } + if (proxy instanceof MapperInvocationHandler) { + proxy = ReflectUtil.getFieldValue(proxy, "mapper"); + proxy = ReflectUtil.getFieldValue(proxy, "h"); + } + Class mapperClass = (Class) ReflectUtil.getFieldValue(proxy, "mapperInterface"); + if (mapperClass == null) { + try { + mapperProxy = (BaseDaoMapper) + ((Advised) ReflectUtil.getFieldValue(proxy, "advised")).getTargetSource().getTarget(); + proxy = ReflectUtil.getFieldValue(mapperProxy, "h"); + if (proxy instanceof MapperInvocationHandler) { + proxy = ReflectUtil.getFieldValue(proxy, "mapper"); + proxy = ReflectUtil.getFieldValue(proxy, "h"); + } + mapperClass = (Class) ReflectUtil.getFieldValue(proxy, "mapperInterface"); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + if (BooleanUtil.isTrue(properties.getEnabledTenantFilter())) { + loadTenantFilterData(mapperClass); + } + if (BooleanUtil.isTrue(properties.getEnabledDataPermFilter())) { + EnableDataPerm rule = mapperClass.getAnnotation(EnableDataPerm.class); + if (rule != null) { + loadDataPermFilterRules(mapperClass, rule); + } + } + } + } + + private void loadTenantFilterData(Class mapperClass) { + Class modelClass = (Class) ((ParameterizedType) + mapperClass.getGenericInterfaces()[0]).getActualTypeArguments()[0]; + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field field : fields) { + if (field.getAnnotation(TenantFilterColumn.class) != null) { + ModelTenantInfo tenantInfo = new ModelTenantInfo(); + tenantInfo.setModelName(modelClass.getSimpleName()); + tenantInfo.setTableName(modelClass.getAnnotation(Table.class).value()); + tenantInfo.setFieldName(field.getName()); + tenantInfo.setColumnName(MyModelUtil.mapToColumnName(field, modelClass)); + // 判断当前dao中是否包括不需要自动注入租户Id过滤的方法。 + DisableTenantFilter disableTenantFilter = mapperClass.getAnnotation(DisableTenantFilter.class); + if (disableTenantFilter != null) { + // 这里开始获取当前Mapper已经声明的的SqlId中,有哪些是需要排除在外的。 + // 排除在外的将不进行数据过滤。 + Set excludeMethodNameSet = new HashSet<>(); + for (String excludeName : disableTenantFilter.includeMethodName()) { + excludeMethodNameSet.add(excludeName); + // 这里是给pagehelper中,分页查询先获取数据总量的查询。 + excludeMethodNameSet.add(excludeName + "_COUNT"); + } + tenantInfo.setExcludeMethodNameSet(excludeMethodNameSet); + } + cachedTenantMap.put(mapperClass.getName(), tenantInfo); + break; + } + } + } + + private void loadDataPermFilterRules(Class mapperClass, EnableDataPerm rule) { + String sysDataPermMapperName = "SysDataPermMapper"; + // 由于给数据权限Mapper添加@EnableDataPerm,将会导致无限递归,因此这里检测到之后, + // 会在系统启动加载监听器的时候,及时抛出异常。 + if (StrUtil.equals(sysDataPermMapperName, mapperClass.getSimpleName())) { + throw new IllegalStateException("Add @EnableDataPerm annotation to SysDataPermMapper is ILLEGAL!"); + } + // 这里开始获取当前Mapper已经声明的的SqlId中,有哪些是需要排除在外的。 + // 排除在外的将不进行数据过滤。 + Set excludeMethodNameSet = null; + String[] excludes = rule.excluseMethodName(); + if (excludes.length > 0) { + excludeMethodNameSet = new HashSet<>(); + for (String excludeName : excludes) { + excludeMethodNameSet.add(excludeName); + // 这里是给pagehelper中,分页查询先获取数据总量的查询。 + excludeMethodNameSet.add(excludeName + "_COUNT"); + } + } + // 获取Mapper关联的主表信息,包括表名,user过滤字段名和dept过滤字段名。 + Class modelClazz = (Class) + ((ParameterizedType) mapperClass.getGenericInterfaces()[0]).getActualTypeArguments()[0]; + Field[] fields = ReflectUtil.getFields(modelClazz); + Field userFilterField = null; + Field deptFilterField = null; + for (Field field : fields) { + if (null != field.getAnnotation(UserFilterColumn.class)) { + userFilterField = field; + } + if (null != field.getAnnotation(DeptFilterColumn.class)) { + deptFilterField = field; + } + if (userFilterField != null && deptFilterField != null) { + break; + } + } + // 通过注解解析与Mapper关联的Model,并获取与数据权限关联的信息,并将结果缓存。 + ModelDataPermInfo info = new ModelDataPermInfo(); + info.setMainTableName(MyModelUtil.mapToTableName(modelClazz)); + info.setMustIncludeUserRule(rule.mustIncludeUserRule()); + info.setExcludeMethodNameSet(excludeMethodNameSet); + if (userFilterField != null) { + info.setUserFilterColumn(MyModelUtil.mapToColumnName(userFilterField, modelClazz)); + } + if (deptFilterField != null) { + info.setDeptFilterColumn(MyModelUtil.mapToColumnName(deptFilterField, modelClazz)); + } + cachedDataPermMap.put(mapperClass.getName(), info); + } + + @Override + public Object intercept(Invocation invocation) throws Throwable { + // 判断当前线程本地存储中,业务操作是否禁用了数据权限过滤,如果禁用,则不进行后续的数据过滤处理了。 + if (!GlobalThreadLocal.enabledDataFilter() + && BooleanUtil.isFalse(properties.getEnabledTenantFilter())) { + return invocation.proceed(); + } + // 只有在HttpServletRequest场景下,该拦截器才起作用,对于系统级别的预加载数据不会应用数据权限。 + if (!ContextUtil.hasRequestContext()) { + return invocation.proceed(); + } + // 没有登录的用户,不会参与租户过滤,如果需要过滤的,自己在代码中手动实现 + // 通常对于无需登录的白名单url,也无需过滤了。 + // 另外就是登录接口中,获取菜单列表的接口,由于尚未登录,没有TokenData,所以这个接口我们手动加入了该条件。 + if (TokenData.takeFromRequest() == null) { + return invocation.proceed(); + } + FlexStatementHandler handler = null; + try { + handler = (FlexStatementHandler) invocation.getTarget(); + } catch (Exception e) { + handler = (FlexStatementHandler) + ReflectUtil.getFieldValue(ReflectUtil.getFieldValue(invocation.getTarget(), "h"), "target"); + } + StatementHandler delegate = + (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate"); + // 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性 + MappedStatement mappedStatement = + (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement"); + SqlCommandType commandType = mappedStatement.getSqlCommandType(); + // 对于INSERT语句,我们不进行任何数据过滤。 + if (commandType == SqlCommandType.INSERT) { + return invocation.proceed(); + } + String sqlId = mappedStatement.getId(); + int pos = StrUtil.lastIndexOfIgnoreCase(sqlId, "."); + String className = StrUtil.sub(sqlId, 0, pos); + String methodName = StrUtil.subSuf(sqlId, pos + 1); + // 先进行租户过滤条件的处理,再将解析并处理后的SQL Statement交给下一步的数据权限过滤去处理。 + // 这样做的目的主要是为了减少一次SQL解析的过程,因为这是高频操作,所以要尽量去优化。 + Statement statement = null; + if (BooleanUtil.isTrue(properties.getEnabledTenantFilter())) { + statement = this.processTenantFilter(className, methodName, delegate.getBoundSql(), commandType); + } + // 处理数据权限过滤。 + if (GlobalThreadLocal.enabledDataFilter() + && BooleanUtil.isTrue(properties.getEnabledDataPermFilter())) { + this.processDataPermFilter(className, methodName, delegate.getBoundSql(), commandType, statement, sqlId); + } + return invocation.proceed(); + } + + private Statement processTenantFilter( + String className, String methodName, BoundSql boundSql, SqlCommandType commandType) throws JSQLParserException { + ModelTenantInfo info = cachedTenantMap.get(className); + if (info == null || CollUtil.contains(info.getExcludeMethodNameSet(), methodName)) { + return null; + } + String sql = boundSql.getSql(); + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder filterBuilder = new StringBuilder(64); + filterBuilder.append(info.tableName).append(".") + .append(info.columnName) + .append("=") + .append(TokenData.takeFromRequest().getTenantId()); + String dataFilter = filterBuilder.toString(); + if (commandType == SqlCommandType.UPDATE) { + Update update = (Update) statement; + this.buildWhereClause(update, dataFilter); + } else if (commandType == SqlCommandType.DELETE) { + Delete delete = (Delete) statement; + this.buildWhereClause(delete, dataFilter); + } else { + Select select = (Select) statement; + PlainSelect selectBody = (PlainSelect) select.getSelectBody(); + FromItem fromItem = selectBody.getFromItem(); + if (fromItem != null) { + PlainSelect subSelect = null; + if (fromItem instanceof SubSelect) { + subSelect = (PlainSelect) ((SubSelect) fromItem).getSelectBody(); + } + if (subSelect != null) { + dataFilter = replaceTableAlias(info.getTableName(), subSelect, dataFilter); + buildWhereClause(subSelect, dataFilter); + } else { + dataFilter = replaceTableAlias(info.getTableName(), selectBody, dataFilter); + buildWhereClause(selectBody, dataFilter); + } + } + } + log.info("Tenant Filter Where Clause [{}]", dataFilter); + ReflectUtil.setFieldValue(boundSql, "sql", statement.toString()); + return statement; + } + + private void processDataPermFilter( + String className, String methodName, BoundSql boundSql, SqlCommandType commandType, Statement statement, String sqlId) + throws JSQLParserException { + // 判断当前线程本地存储中,业务操作是否禁用了数据权限过滤,如果禁用,则不进行后续的数据过滤处理了。 + // 数据过滤权限中,INSERT不过滤。如果是管理员则不参与数据权限的数据过滤,显示全部数据。 + TokenData tokenData = TokenData.takeFromRequest(); + if (Boolean.TRUE.equals(tokenData.getIsAdmin())) { + return; + } + ModelDataPermInfo info = cachedDataPermMap.get(className); + // 再次查找当前方法是否为排除方法,如果不是,就参与数据权限注入过滤。 + if (info == null || CollUtil.contains(info.getExcludeMethodNameSet(), methodName)) { + return; + } + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + Object cachedData = this.getCachedData(dataPermSessionKey); + if (cachedData == null) { + throw new NoDataPermException(StrFormatter.format( + "No Related DataPerm found for SQL_ID [{}] from Cache.", sqlId)); + } + JSONObject allMenuDataPermMap = cachedData instanceof JSONObject + ? (JSONObject) cachedData : JSON.parseObject(cachedData.toString()); + JSONObject menuDataPermMap = this.getAndVerifyMenuDataPerm(allMenuDataPermMap, sqlId); + Map dataPermMap = new HashMap<>(8); + for (Map.Entry entry : menuDataPermMap.entrySet()) { + dataPermMap.put(Integer.valueOf(entry.getKey()), entry.getValue().toString()); + } + if (MapUtil.isEmpty(dataPermMap)) { + throw new NoDataPermException(StrFormatter.format( + "No Related DataPerm found for SQL_ID [{}].", sqlId)); + } + if (dataPermMap.containsKey(DataPermRuleType.TYPE_ALL)) { + return; + } + // 如果当前过滤注解中mustIncludeUserRule参数为true,同时当前用户的数据权限中,不包含TYPE_USER_ONLY, + // 这里就需要自动添加该数据权限。 + if (info.getMustIncludeUserRule() + && !dataPermMap.containsKey(DataPermRuleType.TYPE_USER_ONLY)) { + dataPermMap.put(DataPermRuleType.TYPE_USER_ONLY, null); + } + this.processDataPerm(info, dataPermMap, boundSql, commandType, statement); + } + + private JSONObject getAndVerifyMenuDataPerm(JSONObject allMenuDataPermMap, String sqlId) { + String menuId = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_MENU_ID); + if (menuId == null) { + menuId = ContextUtil.getHttpRequest().getParameter(ApplicationConstant.HTTP_HEADER_MENU_ID); + } + if (BooleanUtil.isFalse(properties.getEnableMenuPermVerify()) && menuId == null) { + menuId = ApplicationConstant.DATA_PERM_ALL_MENU_ID; + } + Assert.notNull(menuId); + JSONObject menuDataPermMap = allMenuDataPermMap.getJSONObject(menuId); + if (menuDataPermMap == null) { + menuDataPermMap = allMenuDataPermMap.getJSONObject(ApplicationConstant.DATA_PERM_ALL_MENU_ID); + } + if (menuDataPermMap == null) { + throw new NoDataPermException(StrFormatter.format( + "No Related DataPerm found for menuId [{}] and SQL_ID [{}].", menuId, sqlId)); + } + if (BooleanUtil.isTrue(properties.getEnableMenuPermVerify())) { + String url = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_ORIGINAL_REQUEST_URL); + if (StrUtil.isBlank(url)) { + url = ContextUtil.getHttpRequest().getRequestURI(); + } + Assert.notNull(url); + if (!this.verifyMenuPerm(null, url, sqlId) && !this.verifyMenuPerm(menuId, url, sqlId)) { + String msg = StrFormatter.format("Mismatched DataPerm " + + "for menuId [{}] and url [{}] and SQL_ID [{}].", menuId, url, sqlId); + throw new NoDataPermException(msg); + } + } + return menuDataPermMap; + } + + private Object getCachedData(String dataPermSessionKey) { + Object cachedData; + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.DATA_PERMISSION_CACHE.name()); + org.springframework.util.Assert.notNull(cache, "Cache [DATA_PERMISSION_CACHE] can't be null."); + Cache.ValueWrapper wrapper = cache.get(dataPermSessionKey); + if (wrapper == null) { + cachedData = redissonClient.getBucket(dataPermSessionKey).get(); + if (cachedData != null) { + cache.put(dataPermSessionKey, JSON.parseObject(cachedData.toString())); + } + } else { + cachedData = wrapper.get(); + } + return cachedData; + } + + @SuppressWarnings("unchecked") + private boolean verifyMenuPerm(String menuId, String url, String sqlId) { + String sessionId = TokenData.takeFromRequest().getSessionId(); + String menuPermSessionKey; + if (menuId != null) { + menuPermSessionKey = RedisKeyUtil.makeSessionMenuPermKey(sessionId, menuId); + } else { + menuPermSessionKey = RedisKeyUtil.makeSessionWhiteListPermKey(sessionId); + } + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.MENU_PERM_CACHE.name()); + org.springframework.util.Assert.notNull(cache, "Cache [MENU_PERM_CACHE] can't be null!"); + Cache.ValueWrapper wrapper = cache.get(menuPermSessionKey); + if (wrapper != null) { + Object cachedData = wrapper.get(); + if (cachedData != null) { + return ((Set) cachedData).contains(url); + } + } + RBucket bucket = redissonClient.getBucket(menuPermSessionKey); + if (!bucket.isExists()) { + String msg; + if (menuId == null) { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for WHITE_LIST and SQL_ID [{}] with sessionId [{}].", sqlId, sessionId); + } else { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for menuId [{}] and SQL_ID [{}] with sessionId [{}].", menuId, sqlId, sessionId); + } + throw new NoDataPermException(msg); + } + Set cachedMenuPermSet = new HashSet<>(JSONArray.parseArray(bucket.get(), String.class)); + cache.put(menuPermSessionKey, cachedMenuPermSet); + return cachedMenuPermSet.contains(url); + } + + private void processDataPerm( + ModelDataPermInfo info, + Map dataPermMap, + BoundSql boundSql, + SqlCommandType commandType, + Statement statement) throws JSQLParserException { + List criteriaList = new LinkedList<>(); + for (Map.Entry entry : dataPermMap.entrySet()) { + String filterClause = processDataPermRule(info, entry.getKey(), entry.getValue()); + if (StrUtil.isNotBlank(filterClause)) { + criteriaList.add(filterClause); + } + } + if (CollUtil.isEmpty(criteriaList)) { + return; + } + StringBuilder filterBuilder = new StringBuilder(128); + filterBuilder.append("("); + filterBuilder.append(StrUtil.join(" OR ", criteriaList)); + filterBuilder.append(")"); + String dataFilter = filterBuilder.toString(); + if (statement == null) { + String sql = boundSql.getSql(); + statement = CCJSqlParserUtil.parse(sql); + } + if (commandType == SqlCommandType.UPDATE) { + Update update = (Update) statement; + this.buildWhereClause(update, dataFilter); + } else if (commandType == SqlCommandType.DELETE) { + Delete delete = (Delete) statement; + this.buildWhereClause(delete, dataFilter); + } else { + this.processSelect(statement, info, dataFilter); + } + log.info("DataPerm Filter Where Clause [{}]", dataFilter); + ReflectUtil.setFieldValue(boundSql, "sql", statement.toString()); + } + + private void processSelect(Statement statement, ModelDataPermInfo info, String dataFilter) + throws JSQLParserException { + Select select = (Select) statement; + PlainSelect selectBody = (PlainSelect) select.getSelectBody(); + FromItem fromItem = selectBody.getFromItem(); + if (fromItem == null) { + return; + } + PlainSelect subSelect = null; + if (fromItem instanceof SubSelect) { + subSelect = (PlainSelect) ((SubSelect) fromItem).getSelectBody(); + } + if (subSelect != null) { + dataFilter = replaceTableAlias(info.getMainTableName(), subSelect, dataFilter); + buildWhereClause(subSelect, dataFilter); + } else { + dataFilter = replaceTableAlias(info.getMainTableName(), selectBody, dataFilter); + buildWhereClause(selectBody, dataFilter); + } + } + + private String processDataPermRule(ModelDataPermInfo info, Integer ruleType, String dataIds) { + TokenData tokenData = TokenData.takeFromRequest(); + StringBuilder filter = new StringBuilder(128); + String tableName = info.getMainTableName(); + if (ruleType != DataPermRuleType.TYPE_USER_ONLY + && ruleType != DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS + && ruleType != DataPermRuleType.TYPE_DEPT_USERS) { + return this.processDeptDataPermRule(info, ruleType, dataIds); + } + if (StrUtil.isBlank(info.getUserFilterColumn())) { + log.warn("No UserFilterColumn for table [{}] but USER_FILTER_DATA_PERM exists !!!", tableName); + return filter.toString(); + } + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + if (ruleType == DataPermRuleType.TYPE_USER_ONLY) { + filter.append(info.getUserFilterColumn()) + .append(" = ") + .append(tokenData.getUserId()); + } else { + filter.append(info.getUserFilterColumn()) + .append(" IN (") + .append(dataIds) + .append(") "); + } + return filter.toString(); + } + + private String processDeptDataPermRule(ModelDataPermInfo info, Integer ruleType, String deptIds) { + StringBuilder filter = new StringBuilder(128); + String tableName = info.getMainTableName(); + if (StrUtil.isBlank(info.getDeptFilterColumn())) { + log.warn("No DeptFilterColumn for table [{}] but DEPT_FILTER_DATA_PERM exists !!!", tableName); + return filter.toString(); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (ruleType == DataPermRuleType.TYPE_DEPT_ONLY) { + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" = ") + .append(tokenData.getDeptId()); + } else if (ruleType == DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id = ") + .append(tokenData.getDeptId()) + .append(" AND "); + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" = ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id IN (") + .append(deptIds) + .append(") AND "); + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" = ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) { + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" IN (") + .append(deptIds) + .append(") "); + } + return filter.toString(); + } + + private String replaceTableAlias(String tableName, PlainSelect select, String dataFilter) { + if (select.getFromItem().getAlias() == null) { + return dataFilter; + } + return dataFilter.replaceAll(tableName, select.getFromItem().getAlias().getName()); + } + + private void buildWhereClause(Update update, String dataFilter) throws JSQLParserException { + if (update.getWhere() == null) { + update.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter)); + } else { + AndExpression and = new AndExpression( + CCJSqlParserUtil.parseCondExpression(dataFilter), update.getWhere()); + update.setWhere(and); + } + } + + private void buildWhereClause(Delete delete, String dataFilter) throws JSQLParserException { + if (delete.getWhere() == null) { + delete.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter)); + } else { + AndExpression and = new AndExpression( + CCJSqlParserUtil.parseCondExpression(dataFilter), delete.getWhere()); + delete.setWhere(and); + } + } + + private void buildWhereClause(PlainSelect select, String dataFilter) throws JSQLParserException { + if (select.getWhere() == null) { + select.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter)); + } else { + AndExpression and = new AndExpression( + CCJSqlParserUtil.parseCondExpression(dataFilter), select.getWhere()); + select.setWhere(and); + } + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + // 这里需要空注解,否则sonar会不happy。 + } + + @Data + private static final class ModelDataPermInfo { + private Set excludeMethodNameSet; + private String userFilterColumn; + private String deptFilterColumn; + private String mainTableName; + private Boolean mustIncludeUserRule; + } + + @Data + private static final class ModelTenantInfo { + private Set excludeMethodNameSet; + private String modelName; + private String tableName; + private String fieldName; + private String columnName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java new file mode 100644 index 00000000..5d7cb78b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.datafilter.listener; + +import com.orangeforms.common.datafilter.interceptor.MybatisDataFilterInterceptor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +/** + * 应用服务启动监听器。 + * 目前主要功能是调用MybatisDataFilterInterceptor中的loadInfoWithDataFilter方法, + * 将标记有过滤注解的数据加载到缓存,以提升系统运行时效率。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class LoadDataFilterInfoListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + MybatisDataFilterInterceptor interceptor = + applicationReadyEvent.getApplicationContext().getBean(MybatisDataFilterInterceptor.class); + interceptor.loadInfoWithDataFilter(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..a08c930a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.datafilter.config.DataFilterAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/pom.xml new file mode 100644 index 00000000..e7ba325b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/pom.xml @@ -0,0 +1,54 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-dbutil + 1.0.0 + common-dbutil + jar + + + + com.orangeforms + common-core + 1.0.0 + + + mysql + mysql-connector-java + 8.0.22 + + + org.postgresql + postgresql + runtime + + + com.oracle.database.jdbc + ojdbc6 + 11.2.0.4 + + + com.dameng + DmJdbcDriver18 + 8.1.2.141 + + + org.opengauss + opengauss-jdbc + 5.0.0-og + + + ru.yandex.clickhouse + clickhouse-jdbc + 0.3.2 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java new file mode 100644 index 00000000..258b9a73 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.dbutil.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 自定义日期过滤值类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class CustomDateValueType { + /** + * 本日。 + */ + public static final String CURRENT_DAY = "1"; + /** + * 本周。 + */ + public static final String CURRENT_WEEK = "2"; + /** + * 本月。 + */ + public static final String CURRENT_MONTH = "3"; + /** + * 本季度。 + */ + public static final String CURRENT_QUARTER = "4"; + /** + * 今年。 + */ + public static final String CURRENT_YEAR = "5"; + /** + * 昨天。 + */ + public static final String LAST_DAY = "11"; + /** + * 上周。 + */ + public static final String LAST_WEEK = "12"; + /** + * 上月。 + */ + public static final String LAST_MONTH = "13"; + /** + * 上季度。 + */ + public static final String LAST_QUARTER = "14"; + /** + * 去年。 + */ + public static final String LAST_YEAR = "15"; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(CURRENT_DAY, "本日"); + DICT_MAP.put(CURRENT_WEEK, "本周"); + DICT_MAP.put(CURRENT_MONTH, "本月"); + DICT_MAP.put(CURRENT_QUARTER, "本季度"); + DICT_MAP.put(CURRENT_YEAR, "今年"); + DICT_MAP.put(LAST_DAY, "昨日"); + DICT_MAP.put(LAST_WEEK, "上周"); + DICT_MAP.put(LAST_MONTH, "上月"); + DICT_MAP.put(LAST_QUARTER, "上季度"); + DICT_MAP.put(LAST_YEAR, "去年"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(String value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private CustomDateValueType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java new file mode 100644 index 00000000..83c2ecef --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java @@ -0,0 +1,74 @@ +package com.orangeforms.common.dbutil.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据库连接类型常量对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DblinkType { + + /** + * MySQL。 + */ + public static final int MYSQL = 0; + /** + * PostgreSQL。 + */ + public static final int POSTGRESQL = 1; + /** + * Oracle。 + */ + public static final int ORACLE = 2; + /** + * Dameng。 + */ + public static final int DAMENG = 3; + /** + * 人大金仓。 + */ + public static final int KINGBASE = 4; + /** + * OpenGauss。 + */ + public static final int OPENGAUSS = 5; + /** + * ClickHouse。 + */ + public static final int CLICKHOUSE = 10; + /** + * Doris。 + */ + public static final int DORIS = 11; + + private static final Map DICT_MAP = new HashMap<>(3); + static { + DICT_MAP.put(MYSQL, "MySQL"); + DICT_MAP.put(POSTGRESQL, "PostgreSQL"); + DICT_MAP.put(ORACLE, "Oracle"); + DICT_MAP.put(DAMENG, "Dameng"); + DICT_MAP.put(KINGBASE, "人大金仓"); + DICT_MAP.put(OPENGAUSS, "OpenGauss"); + DICT_MAP.put(CLICKHOUSE, "ClickHouse"); + DICT_MAP.put(DORIS, "Doris"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DblinkType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java new file mode 100644 index 00000000..8ec9d20a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.dbutil.object; + +import com.orangeforms.common.core.constant.FieldFilterType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; + +/** + * 数据集过滤对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class DatasetFilter extends ArrayList { + + @Data + public static class FilterInfo { + /** + * 过滤的数据集Id。 + */ + private Long datasetId; + /** + * 过滤参数名称。 + */ + private String paramName; + /** + * 过滤参数值是单值时。使用该字段值。 + */ + private Object paramValue; + /** + * 过滤参数值是集合时,使用该字段值。 + */ + private Collection paramValueList; + /** + * 过滤类型。参考常量类 FieldFilterType。 + */ + private Integer filterType = FieldFilterType.EQUAL; + /** + * 是否为日期值的过滤。 + */ + private Boolean dateValueFilter = false; + /** + * 日期精确到。year/month/week/day + */ + private String dateRange; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java new file mode 100644 index 00000000..03886f41 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.dbutil.object; + +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageParam; +import lombok.Data; + +import java.util.List; + +/** + * 数据集查询的各种参数。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class DatasetParam { + + /** + * SELECT选择的字段名列表。 + */ + private List selectColumnNameList; + /** + * 数据集过滤参数。 + */ + private DatasetFilter filter; + /** + * SQL结果集的参数。 + */ + private DatasetFilter sqlFilter; + /** + * 分页参数。 + */ + private MyPageParam pageParam; + /** + * 分组参数。 + */ + private MyOrderParam orderParam; + /** + * 排序字符串。 + */ + private String orderBy; + /** + * 该值目前仅用于SQL类型的结果集。 + * 如果该值为true,SQL结果集中定义的参数都会被替换为 (1 = 1) 的恒成立过滤。 + * 比如 select * from zz_sys_user where user_status = ${status}, + * 该值为true的时会被替换为 select * from zz_sys_user where 1 = 1。 + */ + private Boolean disableSqlDatasetFilter = false; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java new file mode 100644 index 00000000..f3151866 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.dbutil.object; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 报表通用的查询结果集对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GenericResultSet { + + /** + * 查询结果集的字段meta数据列表。 + */ + private List columnMetaList; + + /** + * 查询数据集。如果当前结果集为分页查询,将只包含分页数据。 + */ + private List dataList; + + /** + * 查询数据总数。如果当前结果集为分页查询,该值为分页前的数据总数,否则为0。 + */ + private Long totalCount = 0L; + + public GenericResultSet(List columnMetaList, List dataList) { + this.columnMetaList = columnMetaList; + this.dataList = dataList; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java new file mode 100644 index 00000000..7c927194 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.dbutil.object; + +import cn.hutool.core.collection.CollUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 直接从数据库获取的查询结果集对象。通常内部使用。 + * + * @author Jerry + * @date 2024-07-02 + */ +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Data +public class SqlResultSet extends GenericResultSet { + + public SqlResultSet(List columnMetaList, List dataList) { + super(columnMetaList, dataList); + } + + public static boolean isEmpty(SqlResultSet rs) { + return rs == null || CollUtil.isEmpty(rs.getDataList()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java new file mode 100644 index 00000000..fdda9cf8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.dbutil.object; + +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * 数据库中的表对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SqlTable { + + /** + * 表名称。 + */ + private String tableName; + + /** + * 表注释。 + */ + private String tableComment; + + /** + * 创建时间。 + */ + private Date createTime; + + /** + * 关联的字段列表。 + */ + private List columnList; + + /** + * 数据库链接Id。 + */ + private Long dblinkId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java new file mode 100644 index 00000000..afd5763f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.dbutil.object; + +import lombok.Data; + +/** + * 数据库中的表字段对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SqlTableColumn { + + /** + * 表字段名。 + */ + private String columnName; + + /** + * 字段注释。 + */ + private String columnComment; + + /** + * 表字段类型。 + */ + private String columnType; + + /** + * 表字段全类型。 + */ + private String fullColumnType; + + /** + * 是否自动增长。 + */ + private Boolean autoIncrement; + + /** + * 是否为主键。 + */ + private Boolean primaryKey; + + /** + * 是否可以为空值。 + */ + private Boolean nullable; + + /** + * 字段顺序。 + */ + private Integer columnShowOrder; + + /** + * 附加信息。 + */ + private String extra; + + /** + * 数值型字段精度。 + */ + private Integer numericPrecision; + + /** + * 数值型字段刻度。 + */ + private Integer numericScale; + + /** + * 字符型字段精度。 + */ + private Long stringPrecision; + + /** + * 缺省值。 + */ + private Object columnDefault; + + /** + * 数据库链接类型。该值为冗余字段,只是为了提升运行时效率。 + */ + private int dblinkType; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java new file mode 100644 index 00000000..c0a2423f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java @@ -0,0 +1,108 @@ +package com.orangeforms.common.dbutil.provider; + +/** + * 数据源操作的提供者接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface DataSourceProvider { + + /** + * 返回数据库链接类型,具体值可参考DblinkType常量类。 + * @return 返回数据库链接类型 + */ + int getDblinkType(); + + /** + * 返回Jdbc的配置对象。 + * + * @param configuration Jdbc 的配置数据,JSON格式。 + * @return Jdbc的配置对象。 + */ + JdbcConfig getJdbcConfig(String configuration); + + /** + * 获取当前数据库表meta列表数据的SQL语句。 + * + * @param searchString 表名的模糊匹配字符串。如果为空,则没有前缀规律。 + * @return 查询数据库表meta列表数据的SQL语句。 + */ + String getTableMetaListSql(String searchString); + + /** + * 获取当前数据库表meta数据的SQL语句。 + * + * @return 查询数据库表meta数据的SQL语句。 + */ + String getTableMetaSql(); + + /** + * 获取当前数据库指定表字段meta列表数据的SQL语句。 + * + * @return 查询指定表字段meta列表数据的SQL语句。 + */ + String getTableColumnMetaListSql(); + + /** + * 获取测试数据库连接的查询SQL。 + * + * @return 测试数据库连接的查询SQL + */ + default String getTestQuery() { + return "SELECT 'x'"; + } + + /** + * 为当前的SQL参数,加上分页部分。 + * + * @param sql SQL查询语句。 + * @param pageNum 页号,从1开始。 + * @param pageSize 每页数据量,如果为null,则取出后面所有数据。 + * @return 加上分页功能的SQL语句。 + */ + String makePageSql(String sql, Integer pageNum, Integer pageSize); + + /** + * 将数据表字段类型转换为Java字段类型。 + * + * @param columnType 数据表字段类型。 + * @param numericPrecision 数值精度。 + * @param numericScale 数值刻度。 + * @return 转换后的类型。 + */ + String convertColumnTypeToJavaType(String columnType, Integer numericPrecision, Integer numericScale); + + /** + * Having从句中,统计字段参与过滤时,是否可以直接使用别名。 + * + * @return 返回true,支持"HAVING sumOfColumn > 0",返回false,则为"HAVING sum(count) > 0"。 + */ + default boolean havingClauseUsingAlias() { + return true; + } + + /** + * SELECT的字段别名,是否需要加双引号,对于有些数据库,如果不加双引号,就会被数据库进行强制性的规则转义。 + * + * @return 返回true,SELECT grade_id "gradeId",否则 SELECT grade_id gradeId + */ + default boolean aliasWithQuotes() { + return false; + } + + /** + * 获取日期类型过滤条件语句。 + * + * @param columnName 字段名。 + * @param operator 操作符。 + * @return 过滤从句。 + */ + default String makeDateTimeFilterSql(String columnName, String operator) { + StringBuilder s = new StringBuilder(128); + if (columnName == null) { + columnName = ""; + } + return s.append(columnName).append(" ").append(operator).append(" ?").toString(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java new file mode 100644 index 00000000..031b9541 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.dbutil.provider; + +import lombok.Data; + +/** + * JDBC配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class JdbcConfig { + + /** + * 驱动名。由子类提供。 + */ + private String driver; + /** + * 连接池验证查询的语句。 + */ + private String validationQuery = "SELECT 'x'"; + /** + * Jdbc连接串,需要子类提供实现。 + */ + private String jdbcConnectionString; + /** + * 主机名。 + */ + private String host; + /** + * 端口号。 + */ + private Integer port; + /** + * 用户名。 + */ + private String username; + /** + * 密码。 + */ + private String password; + /** + * 数据库名。 + */ + private String database; + /** + * 模式名。 + */ + private String schema; + /** + * 连接池初始大小。 + */ + private int initialPoolSize = 5; + /** + * 连接池最小连接数。 + */ + private int minPoolSize = 5; + /** + * 连接池最大连接数。 + */ + private int maxPoolSize = 50; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java new file mode 100644 index 00000000..cc7558b2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java @@ -0,0 +1,42 @@ +package com.orangeforms.common.dbutil.provider; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * MySQL JDBC配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class MySqlConfig extends JdbcConfig { + + /** + * JDBC 驱动名。 + */ + private String driver = "com.mysql.cj.jdbc.Driver"; + /** + * 数据库JDBC连接串的扩展部分。 + */ + private String extraParams = "?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"; + + /** + * 获取拼好后的JDBC连接串。 + * + * @return 拼好后的JDBC连接串。 + */ + @Override + public String getJdbcConnectionString() { + StringBuilder sb = new StringBuilder(256); + sb.append("jdbc:mysql://") + .append(getHost()) + .append(":") + .append(getPort()) + .append("/") + .append(getDatabase()) + .append(extraParams); + return sb.toString(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java new file mode 100644 index 00000000..e4e52bac --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java @@ -0,0 +1,112 @@ +package com.orangeforms.common.dbutil.provider; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.dbutil.constant.DblinkType; + +/** + * MySQL数据源的提供者实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MySqlProvider implements DataSourceProvider { + + @Override + public int getDblinkType() { + return DblinkType.MYSQL; + } + + @Override + public JdbcConfig getJdbcConfig(String configuration) { + return JSON.parseObject(configuration, MySqlConfig.class); + } + + @Override + public String getTableMetaListSql(String searchString) { + StringBuilder sql = new StringBuilder(); + sql.append(this.getTableMetaListSql()); + if (StrUtil.isNotBlank(searchString)) { + sql.append(" AND table_name LIKE ?"); + } + return sql.append(" ORDER BY table_name").toString(); + } + + @Override + public String getTableMetaSql() { + return this.getTableMetaListSql() + " AND table_name = ?"; + } + + @Override + public String getTableColumnMetaListSql() { + return "SELECT " + + " column_name columnName, " + + " data_type columnType, " + + " column_type fullColumnType, " + + " column_comment columnComment, " + + " CASE WHEN column_key = 'PRI' THEN 1 ELSE 0 END AS primaryKey, " + + " is_nullable nullable, " + + " ordinal_position columnShowOrder, " + + " extra extra, " + + " CHARACTER_MAXIMUM_LENGTH stringPrecision, " + + " numeric_precision numericPrecision, " + + " COLUMN_DEFAULT columnDefault " + + "FROM " + + " information_schema.columns " + + "WHERE " + + " table_name = ?" + + " AND table_schema = (SELECT database()) " + + "ORDER BY ordinal_position"; + } + + @Override + public String makePageSql(String sql, Integer pageNum, Integer pageSize) { + if (pageSize == null) { + pageSize = 10; + } + int offset = pageNum > 0 ? (pageNum - 1) * pageSize : 0; + return sql + " LIMIT " + offset + "," + pageSize; + } + + @Override + public String convertColumnTypeToJavaType(String columnType, Integer numericPrecision, Integer numericScale) { + if (StrUtil.equalsAnyIgnoreCase(columnType, + "varchar", "char", "text", "longtext", "mediumtext", "tinytext", "enum", "json")) { + return ObjectFieldType.STRING; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "int", "mediumint", "smallint", "tinyint")) { + return ObjectFieldType.INTEGER; + } + if (StrUtil.equalsIgnoreCase(columnType, "bit")) { + return ObjectFieldType.BOOLEAN; + } + if (StrUtil.equalsIgnoreCase(columnType, "bigint")) { + return ObjectFieldType.LONG; + } + if (StrUtil.equalsIgnoreCase(columnType, "decimal")) { + return ObjectFieldType.BIG_DECIMAL; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "float", "double")) { + return ObjectFieldType.DOUBLE; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "date", "datetime", "timestamp", "time")) { + return ObjectFieldType.DATE; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "longblob", "blob")) { + return ObjectFieldType.BYTE_ARRAY; + } + return null; + } + + private String getTableMetaListSql() { + return "SELECT " + + " table_name tableName, " + + " table_comment tableComment, " + + " create_time createTime " + + "FROM " + + " information_schema.tables " + + "WHERE " + + " table_schema = DATABASE() "; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java new file mode 100644 index 00000000..752e645e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java @@ -0,0 +1,838 @@ +package com.orangeforms.common.dbutil.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.pool.DruidDataSourceFactory; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.FieldFilterType; +import com.orangeforms.common.core.exception.InvalidDblinkTypeException; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.core.util.MyDateUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.dbutil.constant.DblinkType; +import com.orangeforms.common.dbutil.provider.*; +import com.orangeforms.common.dbutil.constant.CustomDateValueType; +import com.orangeforms.common.dbutil.object.*; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectExpressionItem; +import net.sf.jsqlparser.statement.select.SelectItem; +import org.joda.time.DateTime; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 动态加载的数据源工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class DataSourceUtil { + + private final Lock lock = new ReentrantLock(); + private final Map datasourceMap = MapUtil.newHashMap(); + private static final Map PROVIDER_MAP = new HashMap<>(5); + protected final Map dblinkProviderMap = new ConcurrentHashMap<>(4); + + private static final String SQL_SELECT = " SELECT "; + private static final String SQL_SELECT_FROM = " SELECT * FROM ("; + private static final String SQL_AS_TMP = " ) tmp "; + private static final String SQL_ORDER_BY = " ORDER BY "; + private static final String SQL_AND = " AND "; + private static final String SQL_WHERE = " WHERE "; + private static final String LOG_PREPARING_FORMAT = "==> Preparing: {}"; + private static final String LOG_PARMS_FORMAT = "==> Parameters: {}"; + private static final String LOG_TOTAL_FORMAT = "<== Total: {}"; + + static { + PROVIDER_MAP.put(DblinkType.MYSQL, new MySqlProvider()); + } + + /** + * 由子类实现,根据dblinkId获取数据库链接类型的方法。 + * + * @param dblinkId 数据库链接Id。 + * @return 数据库链接类型。 + */ + protected abstract int getDblinkTypeByDblinkId(Long dblinkId); + + /** + * 由子类实现,根据dblinkId获取数据库链接配置信息的方法。 + * + * @param dblinkId 数据库链接Id。 + * @return 数据库链接配置信息。 + */ + protected abstract String getDblinkConfigurationByDblinkId(Long dblinkId); + + /** + * 获取指定数据库类型的Provider实现类。 + * + * @param dblinkType 数据库类型。 + * @return 指定数据库类型的Provider实现类。 + */ + public DataSourceProvider getProvider(Integer dblinkType) { + return PROVIDER_MAP.get(dblinkType); + } + + /** + * 获取指定数据库链接的Provider实现类。 + * + * @param dblinkId 数据库链接Id。 + * @return 指定数据库类型的Provider实现类。 + */ + public DataSourceProvider getProvider(Long dblinkId) { + int dblinkType = this.getDblinkTypeByDblinkId(dblinkId); + DataSourceProvider provider = PROVIDER_MAP.get(dblinkType); + if (provider == null) { + throw new InvalidDblinkTypeException(dblinkType); + } + return provider; + } + + /** + * 测试数据库链接。 + * + * @param dblinkId 数据库链接Id。 + */ + public void testConnection(Long dblinkId) throws Exception { + DataSourceProvider provider = this.getProvider(dblinkId); + this.query(dblinkId, provider.getTestQuery()); + } + + /** + * 通过JDBC方式测试链接。 + * + * @param databaseType 数据库类型。参考DblinkType常量值。 + * @param host 主机名。 + * @param port 端口号。 + * @param schemaName 模式名。 + * @param databaseName 数据库名。 + * @param username 用户名。 + * @param password 密码。 + */ + public static void testConnection( + int databaseType, + String host, + Integer port, + String schemaName, + String databaseName, + String username, + String password) { + StringBuilder urlBuilder = new StringBuilder(256); + String hostAndPort = host + ":" + port; + urlBuilder.append("jdbc:mysql://") + .append(hostAndPort) + .append("/") + .append(databaseName) + .append("?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"); + try { + Connection conn = DriverManager.getConnection(urlBuilder.toString(), username, password); + conn.close(); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new MyRuntimeException(e.getMessage()); + } + } + + /** + * 根据Dblink对象获取关联的数据源。如果不存在会创建该数据库连接池的数据源, + * 并保存到Map中缓存,下次调用时可直接返回。 + * + * @param dblinkId 数据库链接Id。 + * @return 关联的数据库连接池的数据源。 + */ + public DataSource getDataSource(Long dblinkId) throws Exception { + DataSource dataSource = datasourceMap.get(dblinkId); + if (dataSource != null) { + return dataSource; + } + int dblinkType = this.getDblinkTypeByDblinkId(dblinkId); + DataSourceProvider provider = PROVIDER_MAP.get(dblinkType); + if (provider == null) { + throw new InvalidDblinkTypeException(dblinkType); + } + DruidDataSource druidDataSource = null; + lock.lock(); + try { + dataSource = datasourceMap.get(dblinkId); + if (dataSource != null) { + return dataSource; + } + JdbcConfig jdbcConfig = provider.getJdbcConfig(this.getDblinkConfigurationByDblinkId(dblinkId)); + Properties properties = new Properties(); + druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties); + druidDataSource.setUrl(jdbcConfig.getJdbcConnectionString()); + druidDataSource.setDriverClassName(jdbcConfig.getDriver()); + druidDataSource.setValidationQuery(jdbcConfig.getValidationQuery()); + druidDataSource.setUsername(jdbcConfig.getUsername()); + druidDataSource.setPassword(jdbcConfig.getPassword()); + druidDataSource.setInitialSize(jdbcConfig.getInitialPoolSize()); + druidDataSource.setMinIdle(jdbcConfig.getMinPoolSize()); + druidDataSource.setMaxActive(jdbcConfig.getMaxPoolSize()); + druidDataSource.setConnectionErrorRetryAttempts(2); + druidDataSource.setTimeBetweenConnectErrorMillis(500); + druidDataSource.setBreakAfterAcquireFailure(true); + druidDataSource.init(); + datasourceMap.put(dblinkId, druidDataSource); + return druidDataSource; + } catch (Exception e) { + if (druidDataSource != null) { + druidDataSource.close(); + } + log.error("Failed to create DruidDatasource", e); + throw e; + } finally { + lock.unlock(); + } + } + + /** + * 关闭指定数据库链接Id关联的数据源,同时从缓存中移除该数据源对象。 + * + * @param dblinkId 数据库链接Id。 + */ + public void removeDataSource(Long dblinkId) { + lock.lock(); + try { + DataSource dataSource = datasourceMap.get(dblinkId); + if (dataSource == null) { + return; + } + ((DruidDataSource) dataSource).close(); + datasourceMap.remove(dblinkId); + } finally { + lock.unlock(); + } + } + + /** + * 获取指定数据源的数据库连接对象。 + * + * @param dblinkId 数据库链接Id。 + * @return 数据库连接对象。 + */ + public Connection getConnection(Long dblinkId) throws Exception { + DataSource dataSource = this.getDataSource(dblinkId); + return dataSource == null ? null : dataSource.getConnection(); + } + + /** + * 获取指定数据库链接的数据表列表。 + * + * @param dblinkId 数据库链接Id。 + * @param searchString 表名的模糊匹配字符串。如果为空,则没有前缀规律。 + * @return 数据表对象列表。 + */ + public List getTableList(Long dblinkId, String searchString) { + DataSourceProvider provider = this.getProvider(dblinkId); + List paramList = null; + if (StrUtil.isNotBlank(searchString)) { + paramList = new LinkedList<>(); + paramList.add("%" + searchString + "%"); + } + String querySql = provider.getTableMetaListSql(searchString); + try { + return this.query(dblinkId, querySql, paramList, SqlTable.class); + } catch (Exception e) { + log.error("Failed to call getTableList", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定数据库链接的数据表对象。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名称。 + * @return 数据表对象。 + */ + public SqlTable getTable(Long dblinkId, String tableName) { + DataSourceProvider provider = this.getProvider(dblinkId); + String querySql = provider.getTableMetaSql(); + List paramList = new LinkedList<>(); + paramList.add(tableName); + try { + return this.queryOne(dblinkId, querySql, paramList, SqlTable.class); + } catch (Exception e) { + log.error("Failed to call getTable", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定数据库链接下数据表的字段列表。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名称。 + * @return 数据表的字段列表。 + */ + public List getTableColumnList(Long dblinkId, String tableName) { + try { + DataSource dataSource = this.getDataSource(dblinkId); + try (Connection conn = dataSource.getConnection()) { + return this.getTableColumnList(dblinkId, conn, tableName); + } + } catch (Exception e) { + log.error("Failed to call getTableColumnList", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定数据库链接下数据表的字段列表。 + * + * @param dblinkId 数据库链接Id。 + * @param conn 数据库连接对象。 + * @param tableName 表名称。 + * @return 数据表的字段列表。 + */ + public List getTableColumnList(Long dblinkId, Connection conn, String tableName) { + DataSourceProvider provider = this.getProvider(dblinkId); + String querySql = provider.getTableColumnMetaListSql(); + List paramList = new LinkedList<>(); + paramList.add(tableName); + try { + List> dataList = this.query(conn, querySql, paramList); + return this.toTypedDataList(dataList, SqlTableColumn.class); + } catch (Exception e) { + log.error("Failed to call getTableColumnList", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定表的数据。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名。 + * @param datasetParam 数据集查询参数对象。 + * @return 表的数据结果。 + */ + public SqlResultSet> getTableDataList( + Long dblinkId, String tableName, DatasetParam datasetParam) throws Exception { + SqlTable table = this.getTable(dblinkId, tableName); + if (table == null) { + return null; + } + DataSourceProvider provider = this.getProvider(dblinkId); + if (datasetParam == null) { + datasetParam = new DatasetParam(); + } + String sql = "SELECT * FROM " + tableName; + if (CollUtil.isNotEmpty(datasetParam.getSelectColumnNameList())) { + sql = SQL_SELECT + StrUtil.join(",", datasetParam.getSelectColumnNameList()) + " FROM " + tableName; + } + Tuple2> filterTuple = this.buildWhereClauseByFilters(dblinkId, datasetParam.getFilter()); + sql += filterTuple.getFirst(); + List paramList = filterTuple.getSecond(); + String sqlCount = null; + MyPageParam pageParam = datasetParam.getPageParam(); + if (pageParam != null) { + net.sf.jsqlparser.statement.Statement statement = CCJSqlParserUtil.parse(sql); + Select select = (Select) statement; + PlainSelect selectBody = (PlainSelect) select.getSelectBody(); + List countSelectItems = new LinkedList<>(); + countSelectItems.add(new SelectExpressionItem(new Column("COUNT(1) AS CNT"))); + selectBody.setSelectItems(countSelectItems); + sqlCount = select.toString(); + sql = provider.makePageSql(sql, pageParam.getPageNum(), pageParam.getPageSize()); + } + return this.getDataListInternnally(dblinkId, provider, sqlCount, sql, datasetParam, paramList); + } + + /** + * 在指定数据库链接上执行查询语句,并返回指定映射对象类型的单条数据对象。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @param paramList 参数列表。 + * @param clazz 返回的映射对象Class类型。 + * @return 查询的结果对象。 + */ + public T queryOne(Long dblinkId, String query, List paramList, Class clazz) throws Exception { + List dataList = this.query(dblinkId, query, paramList, clazz); + return CollUtil.isEmpty(dataList) ? null : dataList.get(0); + } + + /** + * 在指定数据库链接上执行查询语句,并返回指定映射对象类型的数据列表。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @param paramList 参数列表。 + * @param clazz 返回的映射对象Class类型。 + * @return 查询的结果集。 + */ + public List query(Long dblinkId, String query, List paramList, Class clazz) throws Exception { + List> dataList = this.query(dblinkId, query, paramList); + return this.toTypedDataList(dataList, clazz); + } + + /** + * 在指定数据库链接上执行查询语句。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @return 查询的结果集。 + */ + public List> query(Long dblinkId, String query) throws Exception { + DataSource dataSource = this.getDataSource(dblinkId); + try (Connection conn = dataSource.getConnection()) { + return this.query(conn, query); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw e; + } + } + + /** + * 在指定数据库链接上执行查询语句。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @param paramList 参数列表。 + * @return 查询的结果集。 + */ + public List> query(Long dblinkId, String query, List paramList) throws Exception { + DataSource dataSource = this.getDataSource(dblinkId); + try (Connection conn = dataSource.getConnection()) { + return this.query(conn, query, paramList); + } + } + + /** + * 计算过滤从句和过滤参数。 + * + * @param dblinkId 数据库链接Id。 + * @param filter 过滤参数列表。 + * @return 返回的Tuple对象的第一个参数是WHERE从句,第二个参数是过滤从句用到的参数列表。 + */ + public Tuple2> buildWhereClauseByFilters(Long dblinkId, DatasetFilter filter) { + filter = this.normalizeFilter(filter); + if (CollUtil.isEmpty(filter)) { + return new Tuple2<>("", null); + } + DataSourceProvider provider = this.getProvider(dblinkId); + StringBuilder where = new StringBuilder(); + int i = 0; + List paramList = new LinkedList<>(); + for (DatasetFilter.FilterInfo filterInfo : filter) { + if (i++ == 0) { + where.append(SQL_WHERE); + } else { + where.append(SQL_AND); + } + this.doBuildWhereClauseByFilter(filterInfo, provider, where, paramList); + } + return new Tuple2<>(where.toString(), paramList); + } + + private void doBuildWhereClauseByFilter( + DatasetFilter.FilterInfo filterInfo, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + where.append(filterInfo.getParamName()); + if (filterInfo.getFilterType().equals(FieldFilterType.EQUAL)) { + this.doBuildWhereClauseByEqualFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.NOT_EQUAL)) { + where.append(" <> ?"); + paramList.add(filterInfo.getParamValue()); + } else if (filterInfo.getFilterType().equals(FieldFilterType.GE)) { + this.doBuildWhereClauseByGeFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.GT)) { + this.doBuildWhereClauseByGtFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.LE)) { + this.doBuildWhereClauseByLeFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.LT)) { + this.doBuildWhereClauseByLtFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.BETWEEN)) { + this.doBuildWhereClauseByBetweenFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.LIKE)) { + where.append(" LIKE ?"); + paramList.add("%" + filterInfo.getParamValue() + "%"); + } else if (filterInfo.getFilterType().equals(FieldFilterType.IN)) { + where.append(" IN ("); + where.append(StrUtil.repeatAndJoin("?", filterInfo.getParamValueList().size(), ",")); + where.append(")"); + paramList.addAll(filterInfo.getParamValueList()); + } else if (filterInfo.getFilterType().equals(FieldFilterType.NOT_IN)) { + where.append(" NOT IN ("); + where.append(StrUtil.repeatAndJoin("?", filterInfo.getParamValueList().size(), ",")); + where.append(")"); + paramList.addAll(filterInfo.getParamValueList()); + } else if (filterInfo.getFilterType().equals(FieldFilterType.IS_NOT_NULL)) { + where.append(" IS NOT NULL"); + } else if (filterInfo.getFilterType().equals(FieldFilterType.IS_NULL)) { + where.append(" IS NULL"); + } + } + + private void doBuildWhereClauseByEqualFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + String beginDateTime = this.getBeginDateTime(filter.getParamValue().toString(), filter.getDateRange()); + String endDateTime = this.getEndDateTime(filter.getParamValue().toString(), filter.getDateRange()); + where.append(provider.makeDateTimeFilterSql(null, ">=")); + where.append(SQL_AND); + where.append(provider.makeDateTimeFilterSql(filter.getParamName(), "<=")); + paramList.add(beginDateTime); + paramList.add(endDateTime); + } else { + where.append(" = ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByGeFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, ">=")); + paramList.add(this.getBeginDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + paramList.add(filter.getParamValue()); + where.append(" >= ?"); + } + } + + private void doBuildWhereClauseByGtFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, ">")); + paramList.add(this.getEndDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + where.append(" > ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByLeFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, "<=")); + paramList.add(this.getEndDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + where.append(" <= ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByLtFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, "<")); + paramList.add(this.getBeginDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + where.append(" < ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByBetweenFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (CollUtil.isEmpty(filter.getParamValueList())) { + return; + } + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + Object[] filterArray = filter.getParamValueList().toArray(); + where.append(provider.makeDateTimeFilterSql(null, ">=")); + paramList.add(this.getBeginDateTime(filterArray[0].toString(), filter.getDateRange())); + where.append(SQL_AND); + where.append(filter.getParamName()); + where.append(provider.makeDateTimeFilterSql(null, "<=")); + paramList.add(this.getEndDateTime(filterArray[1].toString(), filter.getDateRange())); + } else { + where.append(" BETWEEN ? AND ?"); + paramList.add(filter.getParamValueList()); + } + } + + private SqlResultSet> getDataListInternnally( + Long dblinkId, + DataSourceProvider provider, + String sqlCount, + String sql, + DatasetParam datasetParam, + List paramList) throws Exception { + Long totalCount = 0L; + SqlResultSet> resultSet = null; + try (Connection connection = this.getConnection(dblinkId)) { + boolean ignoreQueryData = false; + if (sqlCount != null) { + Map data = this.query(connection, sqlCount, paramList).get(0); + String key = data.entrySet().iterator().next().getKey(); + totalCount = (Long) data.get(key); + if (totalCount == 0L) { + ignoreQueryData = true; + } + } + if (!ignoreQueryData) { + if (datasetParam.getOrderBy() != null) { + sql += SQL_ORDER_BY + datasetParam.getOrderBy(); + } + resultSet = this.queryWithMeta(connection, sql, paramList); + resultSet.setTotalCount(totalCount); + } + } + return resultSet == null ? new SqlResultSet<>() : resultSet; + } + + private List> query(Connection conn, String query) throws SQLException { + try (Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery(query)) { + log.info(LOG_PREPARING_FORMAT, query); + List> resultList = this.fetchResult(rs); + log.info(LOG_TOTAL_FORMAT, resultList.size()); + return resultList; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } + } + + private List> query(Connection conn, String query, List paramList) throws SQLException { + if (CollUtil.isEmpty(paramList)) { + return this.query(conn, query); + } + ResultSet rs = null; + try (PreparedStatement stat = conn.prepareStatement(query)) { + for (int i = 0; i < paramList.size(); i++) { + stat.setObject(i + 1, paramList.get(i)); + } + rs = stat.executeQuery(); + log.info(LOG_PREPARING_FORMAT, query); + List> resultList = this.fetchResult(rs); + log.info(LOG_TOTAL_FORMAT, resultList.size()); + return resultList; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } finally { + if (rs != null) { + try { + rs.close(); + } catch (Exception e) { + log.error("Failed to call rs.close", e); + } + } + } + } + + private SqlResultSet> queryWithMeta( + Connection connection, String query, List paramList) throws SQLException { + if (CollUtil.isEmpty(paramList)) { + try (Statement stat = connection.createStatement(); + ResultSet rs = stat.executeQuery(query)) { + log.info(LOG_PREPARING_FORMAT, query); + SqlResultSet> resultSet = this.fetchResultWithMeta(rs); + log.info(LOG_TOTAL_FORMAT, resultSet.getDataList() == null ? 0 : resultSet.getDataList().size()); + return resultSet; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } + } + ResultSet rs = null; + try (PreparedStatement stat = connection.prepareStatement(query)) { + for (int i = 0; i < paramList.size(); i++) { + stat.setObject(i + 1, paramList.get(i)); + } + rs = stat.executeQuery(); + log.info(LOG_PREPARING_FORMAT, query); + SqlResultSet> resultSet = this.fetchResultWithMeta(rs); + log.info(LOG_TOTAL_FORMAT, resultSet.getDataList() == null ? 0 : resultSet.getDataList().size()); + return resultSet; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } finally { + if (rs != null) { + try { + rs.close(); + } catch (Exception e) { + log.error("Failed to call rs.close", e); + } + } + } + } + + private List> fetchResult(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + List> resultList = new LinkedList<>(); + while (rs.next()) { + JSONObject rowData = new JSONObject(); + for (int i = 0; i < columnCount; i++) { + rowData.put(metaData.getColumnLabel(i + 1), rs.getObject(i + 1)); + } + resultList.add(rowData); + } + return resultList; + } + + private SqlResultSet> fetchResultWithMeta(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + List columnMetaList = new LinkedList<>(); + int columnCount = metaData.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + SqlTableColumn tableColumn = new SqlTableColumn(); + String columnLabel = metaData.getColumnLabel(i + 1); + tableColumn.setColumnName(columnLabel); + tableColumn.setColumnType(metaData.getColumnTypeName(i + 1)); + columnMetaList.add(tableColumn); + } + List> resultList = new LinkedList<>(); + while (rs.next()) { + JSONObject rowData = new JSONObject(); + for (int i = 0; i < columnCount; i++) { + rowData.put(metaData.getColumnLabel(i + 1), rs.getObject(i + 1)); + } + resultList.add(rowData); + } + return new SqlResultSet<>(columnMetaList, resultList); + } + + private List toTypedDataList(List> dataList, Class clazz) { + return MyModelUtil.mapToBeanList(dataList, clazz); + } + + private String getBeginDateTime(String dateValueType, String dateRange) { + DateTime now = DateTime.now(); + switch (dateValueType) { + case CustomDateValueType.CURRENT_DAY: + return MyDateUtil.getBeginTimeOfDayWithShort(now); + case CustomDateValueType.CURRENT_WEEK: + return MyDateUtil.getBeginDateTimeOfWeek(now); + case CustomDateValueType.CURRENT_MONTH: + return MyDateUtil.getBeginDateTimeOfMonth(now); + case CustomDateValueType.CURRENT_YEAR: + return MyDateUtil.getBeginDateTimeOfYear(now); + case CustomDateValueType.CURRENT_QUARTER: + return MyDateUtil.getBeginDateTimeOfQuarter(now); + case CustomDateValueType.LAST_DAY: + return MyDateUtil.getBeginTimeOfDay(now.minusDays(1)); + case CustomDateValueType.LAST_WEEK: + return MyDateUtil.getBeginDateTimeOfWeek(now.minusWeeks(1)); + case CustomDateValueType.LAST_MONTH: + return MyDateUtil.getBeginDateTimeOfMonth(now.minusMonths(1)); + case CustomDateValueType.LAST_YEAR: + return MyDateUtil.getBeginDateTimeOfYear(now.minusYears(1)); + case CustomDateValueType.LAST_QUARTER: + return MyDateUtil.getBeginDateTimeOfQuarter(now.minusMonths(3)); + default: + break; + } + // 执行到这里,基本就是自定义日期数据了 + if (StrUtil.isBlank(dateRange)) { + return dateValueType; + } + DateTime dateValue = MyDateUtil.toDateTimeWithoutMs(dateValueType); + switch (dateRange) { + case "year": + return MyDateUtil.getBeginDateTimeOfYear(dateValue); + case "month": + return MyDateUtil.getBeginDateTimeOfMonth(dateValue); + case "week": + return MyDateUtil.getBeginDateTimeOfWeek(dateValue); + case "date": + return MyDateUtil.getBeginTimeOfDayWithShort(dateValue); + default: + break; + } + return dateValueType; + } + + private String getEndDateTime(String dateValueType, String dateRange) { + DateTime now = DateTime.now(); + switch (dateValueType) { + case CustomDateValueType.CURRENT_DAY: + return MyDateUtil.getEndTimeOfDayWithShort(now); + case CustomDateValueType.CURRENT_WEEK: + return MyDateUtil.getEndDateTimeOfWeek(now); + case CustomDateValueType.CURRENT_MONTH: + return MyDateUtil.getEndDateTimeOfMonth(now); + case CustomDateValueType.CURRENT_YEAR: + return MyDateUtil.getEndDateTimeOfYear(now); + case CustomDateValueType.CURRENT_QUARTER: + return MyDateUtil.getEndDateTimeOfQuarter(now); + case CustomDateValueType.LAST_DAY: + return MyDateUtil.getEndTimeOfDay(now.minusDays(1)); + case CustomDateValueType.LAST_WEEK: + return MyDateUtil.getEndDateTimeOfWeek(now.minusWeeks(1)); + case CustomDateValueType.LAST_MONTH: + return MyDateUtil.getEndDateTimeOfMonth(now.minusMonths(1)); + case CustomDateValueType.LAST_YEAR: + return MyDateUtil.getEndDateTimeOfYear(now.minusYears(1)); + case CustomDateValueType.LAST_QUARTER: + return MyDateUtil.getEndDateTimeOfQuarter(now.minusMonths(3)); + default: + break; + } + // 执行到这里,基本就是自定义日期数据了 + if (StrUtil.isBlank(dateRange)) { + return dateValueType; + } + DateTime dateValue = MyDateUtil.toDateTimeWithoutMs(dateValueType); + switch (dateRange) { + case "year": + return MyDateUtil.getEndDateTimeOfYear(dateValue); + case "month": + return MyDateUtil.getEndDateTimeOfMonth(dateValue); + case "week": + return MyDateUtil.getEndDateTimeOfWeek(dateValue); + case "date": + return MyDateUtil.getEndTimeOfDayWithShort(dateValue); + default: + break; + } + return dateValueType; + } + + private DatasetFilter normalizeFilter(DatasetFilter filter) { + if (CollUtil.isEmpty(filter)) { + return filter; + } + DatasetFilter normalizedFilter = new DatasetFilter(); + for (DatasetFilter.FilterInfo filterInfo : filter) { + if (filterInfo.getFilterType().equals(FieldFilterType.IS_NULL) + || filterInfo.getFilterType().equals(FieldFilterType.IS_NOT_NULL) + || filterInfo.getParamValue() != null + || filterInfo.getParamValueList() != null) { + normalizedFilter.add(filterInfo); + } + } + return normalizedFilter; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-dict/pom.xml new file mode 100644 index 00000000..c2fc5d2d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/pom.xml @@ -0,0 +1,31 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-dict + + + + com.orangeforms + common-redis + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java new file mode 100644 index 00000000..3076abfa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.dict.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 全局字典项目数据状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class GlobalDictItemStatus { + + /** + * 正常。 + */ + public static final int NORMAL = 0; + /** + * 禁用。 + */ + public static final int DISABLED = 1; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(NORMAL, "正常"); + DICT_MAP.put(DISABLED, "禁用"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalDictItemStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java new file mode 100644 index 00000000..640491b6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.GlobalDictItem; + +/** + * 全局字典项目数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictItemMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java new file mode 100644 index 00000000..eb8951e3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.GlobalDict; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 全局字典数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictMapper extends BaseDaoMapper { + + /** + * 获取全局编码字典。 + * @param filter 过滤对象。 + * @param orderBy 排序字符串。 + * @return 全局编码字典。 + */ + @Select("") + List getGlobalDictList(@Param("filter") GlobalDict filter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java new file mode 100644 index 00000000..8a744d02 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 租户全局字典项目数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictItemMapper extends BaseDaoMapper { + + /** + * 批量插入。 + * + * @param dictItemList 字典条目列表。 + */ + @Insert("") + void insertList(@Param("dictItemList") List dictItemList); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java new file mode 100644 index 00000000..6735d704 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.TenantGlobalDict; + +/** + * 租户全局字典数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java new file mode 100644 index 00000000..564655d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java @@ -0,0 +1,40 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.UpdateGroup; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 全局系统字典Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典Dto") +@Data +public class GlobalDictDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long dictId; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + @NotBlank(message = "数据验证失败,字典编码不能为空!") + private String dictCode; + + /** + * 字典中文名称。 + */ + @Schema(description = "字典中文名称") + @NotBlank(message = "数据验证失败,字典中文名称不能为空!") + private String dictName; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java new file mode 100644 index 00000000..e80a934f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.UpdateGroup; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 全局系统字典项目Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典项目Dto") +@Data +public class GlobalDictItemDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long id; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + @NotBlank(message = "数据验证失败,字典编码不能为空!") + private String dictCode; + + /** + * 字典数据项Id。 + */ + @Schema(description = "字典数据项Id") + @NotNull(message = "数据验证失败,字典数据项Id不能为空!") + private String itemId; + + /** + * 字典数据项名称。 + */ + @Schema(description = "字典数据项名称") + @NotBlank(message = "数据验证失败,字典数据项名称不能为空!") + private String itemName; + + /** + * 显示顺序(数值越小越靠前)。 + */ + @Schema(description = "显示顺序") + @NotNull(message = "数据验证失败,显示顺序不能为空!") + private Integer showOrder; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java new file mode 100644 index 00000000..63f55953 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典Dto") +@EqualsAndHashCode(callSuper = true) +@Data +public class TenantGlobalDictDto extends GlobalDictDto { + + /** + * 是否为所有租户的通用字典。 + */ + @Schema(description = "是否为所有租户的通用字典") + private Boolean tenantCommon; + + /** + * 租户的非公用字典的初始化字典数据。 + */ + @Schema(description = "租户的非公用字典的初始化字典数据") + private String initialData; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java new file mode 100644 index 00000000..f6ac99a6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典项目Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典项目Dto") +@EqualsAndHashCode(callSuper = true) +@Data +public class TenantGlobalDictItemDto extends GlobalDictItemDto { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java new file mode 100644 index 00000000..148b1596 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java @@ -0,0 +1,65 @@ +package com.orangeforms.common.dict.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_global_dict") +public class GlobalDict { + + /** + * 主键Id。 + */ + @Id(value = "dict_id") + private Long dictId; + + /** + * 字典编码。 + */ + @Column(value = "dict_code") + private String dictCode; + + /** + * 字典中文名称。 + */ + @Column(value = "dict_name") + private String dictName; + + /** + * 更新用户名。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 创建用户Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 逻辑删除字段。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java new file mode 100644 index 00000000..8e1b3664 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java @@ -0,0 +1,82 @@ +package com.orangeforms.common.dict.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典项目实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_global_dict_item") +public class GlobalDictItem { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 字典编码。 + */ + @Column(value = "dict_code") + private String dictCode; + + /** + * 字典数据项Id。 + */ + @Column(value = "item_id") + private String itemId; + + /** + * 字典数据项名称。 + */ + @Column(value = "item_name") + private String itemName; + + /** + * 显示顺序(数值越小越靠前)。 + */ + @Column(value = "show_order") + private Integer showOrder; + + /** + * 字典状态。具体值引用DictItemStatus常量类。 + */ + private Integer status; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建用户Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新用户名。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 逻辑删除字段。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java new file mode 100644 index 00000000..0a2f16c8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.dict.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Table(value = "zz_tenant_global_dict") +public class TenantGlobalDict extends GlobalDict { + + /** + * 是否为所有租户的通用字典。 + */ + @Column(value = "tenant_common") + private Boolean tenantCommon; + + /** + * 租户的非公用字典的初始化字典数据。 + */ + @Column(value = "initial_data") + private String initialData; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java new file mode 100644 index 00000000..5a637533 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java @@ -0,0 +1,23 @@ +package com.orangeforms.common.dict.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典项目实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Table(value = "zz_tenant_global_dict_item") +public class TenantGlobalDictItem extends GlobalDictItem { + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java new file mode 100644 index 00000000..66750ff7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java @@ -0,0 +1,92 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.GlobalDictItem; + +import java.io.Serializable; +import java.util.List; + +/** + * 全局字典项目数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictItemService extends IBaseService { + + /** + * 保存新增的全局字典项目。 + * + * @param globalDictItem 新字典项目对象。 + * @return 保存后的对象。 + */ + GlobalDictItem saveNew(GlobalDictItem globalDictItem); + + /** + * 更新全局字典项目对象。 + * + * @param globalDictItem 更新的全局字典项目对象。 + * @param originalGlobalDictItem 原有的全局字典项目对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(GlobalDictItem globalDictItem, GlobalDictItem originalGlobalDictItem); + + /** + * 更新字典条目的编码。 + * + * @param oldCode 原有编码。 + * @param newCode 新编码。 + */ + void updateNewCode(String oldCode, String newCode); + + /** + * 更新字典条目的状态。 + * + * @param globalDictItem 字典项目对象。 + * @param status 状态值。 + */ + void updateStatus(GlobalDictItem globalDictItem, Integer status); + + /** + * 删除指定字典项目。 + * + * @param globalDictItem 待删除字典项目。 + * @return 成功返回true,否则false。 + */ + boolean remove(GlobalDictItem globalDictItem); + + /** + * 判断指定的编码和项目Id是否存在。 + * + * @param dictCode 字典编码。 + * @param itemId 项目Id。 + * @return true存在,否则false。 + */ + boolean existDictCodeAndItemId(String dictCode, Serializable itemId); + + /** + * 根据字典编码和项目Id获取指定字段项目对象。 + * + * @param dictCode 字典编码。 + * @param itemId 项目Id。 + * @return 字典项目对象。 + */ + GlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId); + + /** + * 查询数据字典项目列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序字符串,如果为空,则按照showOrder升序排序。 + * @return 查询结果列表。 + */ + List getGlobalDictItemList(GlobalDictItem filter, String orderBy); + + /** + * 查询指定字典编码的数据字典项目列表。查询结果按照showOrder升序排序。 + * + * @param dictCode 过滤对象。 + * @return 查询结果列表。 + */ + List getGlobalDictItemListByDictCode(String dictCode); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java new file mode 100644 index 00000000..2eaadcf2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java @@ -0,0 +1,108 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 全局字典数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictService extends IBaseService { + + /** + * 保存全局字典对象。 + * + * @param globalDict 全局字典对象。 + * @return 保存后的字典对象。 + */ + GlobalDict saveNew(GlobalDict globalDict); + + /** + * 更新全局字典对象。 + * + * @param globalDict 更新的全局字典对象。 + * @param originalGlobalDict 原有的全局字典对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(GlobalDict globalDict, GlobalDict originalGlobalDict); + + /** + * 删除全局字典对象,以及其关联的字典项目数据。 + * + * @param dictId 全局字典Id。 + * @return 是否删除成功。 + */ + boolean remove(Long dictId); + + /** + * 获取全局字典列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序条件。 + * @return 查询结果集列表。 + */ + List getGlobalDictList(GlobalDict filter, String orderBy); + + /** + * 判断字典编码是否存在。 + * + * @param dictCode 字典编码。 + * @return true表示存在,否则false。 + */ + boolean existDictCode(String dictCode); + + /** + * 判断指定字典编码的字典项目是否存在。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemId 字典项目Id。 + * @return true表示存在,否则false。 + */ + boolean existDictItemFromCache(String dictCode, Serializable itemId); + + /** + * 从缓存中获取指定编码的字典项目列表。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + List getGlobalDictItemListFromCache(String dictCode, Set itemIds); + + /** + * 从缓存中获取指定编码的字典项目列表。返回的结果Map中,键是itemId,值是itemName。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + Map getGlobalDictItemDictMapFromCache(String dictCode, Set itemIds); + + /** + * 强制同步指定字典编码的全部字典项目到缓存。 + * + * @param dictCode 字典编码。 + */ + void reloadCachedData(String dictCode); + + /** + * 从缓存中移除指定字典编码的数据。 + * + * @param dictCode 字典编码。 + */ + void removeCache(String dictCode); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java new file mode 100644 index 00000000..74d3f5fa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java @@ -0,0 +1,115 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; + +import java.io.Serializable; +import java.util.List; + +/** + * 租户全局字典项目数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictItemService extends IBaseService { + + /** + * 保存新增的租户字典项目。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 新字典项目对象。 + * @return 保存后的对象。 + */ + TenantGlobalDictItem saveNew(TenantGlobalDict tenantGlobalDict, TenantGlobalDictItem tenantGlobalDictItem); + + /** + * 批量新增的租户字典项目。 + * + * @param dictItemList 字典项对象列表。 + */ + void saveNewBatch(List dictItemList); + + /** + * 更新租户字典项目对象。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 更新的全局字典项目对象。 + * @param originalTenantGlobalDictItem 原有的全局字典项目对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update( + TenantGlobalDict tenantGlobalDict, + TenantGlobalDictItem tenantGlobalDictItem, + TenantGlobalDictItem originalTenantGlobalDictItem); + + /** + * 更新字典条目的编码。 + * + * @param oldCode 原有编码。 + * @param newCode 新编码。 + */ + void updateNewCode(String oldCode, String newCode); + + /** + * 更新字典条目的状态。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 字典项目对象。 + * @param status 状态值。 + */ + void updateStatus(TenantGlobalDict tenantGlobalDict, TenantGlobalDictItem tenantGlobalDictItem, Integer status); + + /** + * 删除指定租户字典项目。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 待删除字典项目。 + * @return 成功返回true,否则false。 + */ + boolean remove(TenantGlobalDict tenantGlobalDict, TenantGlobalDictItem tenantGlobalDictItem); + + /** + * 判断指定字典的项目Id是否存在。如果是租户非公用字典,会基于租户Id进行过滤。 + * + * @param tenantGlobalDict 字典对象。 + * @param itemId 项目Id。 + * @return true存在,否则false。 + */ + boolean existDictCodeAndItemId(TenantGlobalDict tenantGlobalDict, Serializable itemId); + + /** + * 判断指定租户的编码是否已经存在字典数据。 + * + * @param dictCode 字典编码。 + * @return true存在,否则false。 + */ + boolean existDictCode(String dictCode); + + /** + * 根据租户字典编码和项目Id获取指定字段项目对象。 + * + * @param dictCode 字典编码。 + * @param itemId 项目Id。 + * @return 字典项目对象。 + */ + TenantGlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId); + + /** + * 查询租户数据字典项目列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序字符串,如果为空,则按照showOrder升序排序。 + * @return 查询结果列表。 + */ + List getGlobalDictItemList(TenantGlobalDictItem filter, String orderBy); + + /** + * 查询指定字典的租户数据字典项目列表。如果是租户非公用字典,会仅仅返回该租户的字典数据列表。按照showOrder升序排序。 + * + * @param tenantGlobalDict 编码字典对象。 + * @return 查询结果列表。 + */ + List getGlobalDictItemList(TenantGlobalDict tenantGlobalDict); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java new file mode 100644 index 00000000..3c02c46c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java @@ -0,0 +1,137 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 租户全局字典数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictService extends IBaseService { + + /** + * 保存租户全局字典对象。 + * + * @param tenantGlobalDict 全局租户字典对象。 + * @param tenantIdSet 租户Id集合。 + * @return 保存后的字典对象。 + */ + TenantGlobalDict saveNew(TenantGlobalDict tenantGlobalDict, Set tenantIdSet); + + /** + * 更新租户全局字典对象。 + * + * @param tenantGlobalDict 更新的租户全局字典对象。 + * @param originalTenantGlobalDict 原有的租户全局字典对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(TenantGlobalDict tenantGlobalDict, TenantGlobalDict originalTenantGlobalDict); + + /** + * 删除租户全局字典对象,以及其关联的字典项目数据。 + * + * @param dictId 全局字典Id。 + * @return 是否删除成功。 + */ + boolean remove(Long dictId); + + /** + * 获取全局字典列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序条件。 + * @return 查询结果集列表。 + */ + List getGlobalDictList(TenantGlobalDict filter, String orderBy); + + /** + * 判断租户字典编码是否存在。 + * + * @param dictCode 字典编码。 + * @return true表示存在,否则false。 + */ + boolean existDictCode(String dictCode); + + /** + * 根据字典编码获取全局字典编码对象。 + * + * @param dictCode 字典编码。 + * @return 查询后的字典对象。 + */ + TenantGlobalDict getTenantGlobalDictByDictCode(String dictCode); + + /** + * 从缓存中中获取指定字典数据。如果缓存中不存在,会从数据库读取并同步到缓存。 + * + * @param dictCode 字典编码。 + * @return 查询到的字段对象。 + */ + TenantGlobalDict getTenantGlobalDictFromCache(String dictCode); + + /** + * 从缓存中获取指定编码的字典项目列表。 + * 如果是租户非公用字典,会仅仅返回该租户的字典数据列表。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param tenantGlobalDict 编码字典对象。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + List getGlobalDictItemListFromCache(TenantGlobalDict tenantGlobalDict, Set itemIds); + + /** + * 从缓存中获取指定编码的字典项目列表。返回的结果Map中,键是itemId,值是itemName。 + * 如果是租户非公用字典,会仅仅返回该租户的字典数据列表。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param tenantGlobalDict 编码字典对象。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + Map getGlobalDictItemDictMapFromCache(TenantGlobalDict tenantGlobalDict, Set itemIds); + + /** + * 强制同步指定所有租户通用字典编码的全部字典项目到缓存。 + * 如果是租户非公用字典,会仅仅返回该租户的字典数据列表。 + * + * @param tenantGlobalDict 编码字典对象。 + */ + void reloadCachedData(TenantGlobalDict tenantGlobalDict); + + /** + * 重置所有非公用租户编码字典的数据到缓存。 + * 该方法会将指定编码字典中,所有租户的缓存全部重新加载。一般用于系统故障,或大促活动的数据预热。 + * + * @param tenantGlobalDict 非公用编码字典对象。 + */ + void reloadAllTenantCachedData(TenantGlobalDict tenantGlobalDict); + + /** + * 从缓存中移除指定字典编码的数据。 + * 该方法的实现内部会判断是否为公用字典,还是租户可修改的非公用字典。 + * + * @param tenantGlobalDict 字典编码。 + */ + void removeCache(TenantGlobalDict tenantGlobalDict); + + /** + * 判断指定字典编码的字典项目是否存在。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemId 字典项目Id。 + * @return true表示存在,否则false。 + */ + boolean existDictItemFromCache(String dictCode, Serializable itemId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java new file mode 100644 index 00000000..fbf3fd89 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java @@ -0,0 +1,143 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.GlobalDictItemMapper; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictItemService; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 全局字典项目数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE) +@Service("globalDictItemService") +public class GlobalDictItemServiceImpl + extends BaseService implements GlobalDictItemService { + + @Autowired + private GlobalDictItemMapper globalDictItemMapper; + @Autowired + private GlobalDictService globalDictService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return globalDictItemMapper; + } + + @Override + public GlobalDictItem saveNew(GlobalDictItem globalDictItem) { + globalDictService.removeCache(globalDictItem.getDictCode()); + globalDictItem.setId(idGenerator.nextLongId()); + globalDictItem.setDeletedFlag(GlobalDeletedFlag.NORMAL); + globalDictItem.setStatus(GlobalDictItemStatus.NORMAL); + globalDictItem.setCreateUserId(TokenData.takeFromRequest().getUserId()); + globalDictItem.setUpdateUserId(globalDictItem.getCreateUserId()); + globalDictItem.setCreateTime(new Date()); + globalDictItem.setUpdateTime(globalDictItem.getCreateTime()); + globalDictItemMapper.insert(globalDictItem); + return globalDictItem; + } + + @Override + public boolean update(GlobalDictItem globalDictItem, GlobalDictItem originalGlobalDictItem) { + globalDictService.removeCache(globalDictItem.getDictCode()); + // 该方法不能直接修改字典状态。 + globalDictItem.setStatus(originalGlobalDictItem.getStatus()); + globalDictItem.setCreateUserId(originalGlobalDictItem.getCreateUserId()); + globalDictItem.setCreateTime(originalGlobalDictItem.getCreateTime()); + globalDictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + globalDictItem.setUpdateTime(new Date()); + return globalDictItemMapper.update(globalDictItem) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateNewCode(String oldCode, String newCode) { + GlobalDictItem globalDictItem = new GlobalDictItem(); + globalDictItem.setDictCode(newCode); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(GlobalDictItem::getDictCode, oldCode); + globalDictItemMapper.updateByQuery(globalDictItem, queryWrapper); + } + + @Override + public void updateStatus(GlobalDictItem globalDictItem, Integer status) { + globalDictService.removeCache(globalDictItem.getDictCode()); + globalDictItem.setStatus(status); + globalDictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + globalDictItem.setUpdateTime(new Date()); + globalDictItemMapper.update(globalDictItem); + } + + @Override + public boolean remove(GlobalDictItem globalDictItem) { + globalDictService.removeCache(globalDictItem.getDictCode()); + return this.removeById(globalDictItem.getId()); + } + + @Override + public boolean existDictCodeAndItemId(String dictCode, Serializable itemId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(GlobalDictItem::getDictCode, dictCode); + queryWrapper.eq(GlobalDictItem::getItemId, itemId.toString()); + return globalDictItemMapper.selectCountByQuery(queryWrapper) > 0; + } + + @Override + public GlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(GlobalDictItem::getDictCode, dictCode); + queryWrapper.eq(GlobalDictItem::getItemId, itemId.toString()); + return globalDictItemMapper.selectOneByQuery(queryWrapper); + } + + @Override + public List getGlobalDictItemList(GlobalDictItem filter, String orderBy) { + QueryWrapper queryWrapper = filter == null ? QueryWrapper.create() : QueryWrapper.create(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } else { + queryWrapper.orderBy(GlobalDictItem::getShowOrder, true); + } + return globalDictItemMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getGlobalDictItemListByDictCode(String dictCode) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(GlobalDictItem::getDictCode, dictCode); + queryWrapper.orderBy(GlobalDictItem::getShowOrder, true); + return globalDictItemMapper.selectListByQuery(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java new file mode 100644 index 00000000..e6c50e43 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java @@ -0,0 +1,184 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.GlobalDictMapper; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictItemService; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 全局字典数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE) +@Service("globalDictService") +public class GlobalDictServiceImpl extends BaseService implements GlobalDictService { + + @Autowired + private GlobalDictMapper globalDictMapper; + @Autowired + private GlobalDictItemService globalDictItemService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return globalDictMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public GlobalDict saveNew(GlobalDict globalDict) { + globalDict.setDictId(idGenerator.nextLongId()); + globalDict.setDeletedFlag(GlobalDeletedFlag.NORMAL); + globalDict.setCreateUserId(TokenData.takeFromRequest().getUserId()); + globalDict.setUpdateUserId(globalDict.getCreateUserId()); + globalDict.setCreateTime(new Date()); + globalDict.setUpdateTime(globalDict.getCreateTime()); + globalDictMapper.insert(globalDict); + return globalDict; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(GlobalDict globalDict, GlobalDict originalGlobalDict) { + this.removeCache(originalGlobalDict.getDictCode()); + globalDict.setCreateUserId(originalGlobalDict.getCreateUserId()); + globalDict.setCreateTime(originalGlobalDict.getCreateTime()); + globalDict.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + globalDict.setUpdateTime(new Date()); + if (globalDictMapper.update(globalDict) != 1) { + return false; + } + if (!StrUtil.equals(globalDict.getDictCode(), originalGlobalDict.getDictCode())) { + globalDictItemService.updateNewCode(originalGlobalDict.getDictCode(), globalDict.getDictCode()); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dictId) { + GlobalDict globalDict = this.getById(dictId); + if (globalDict == null) { + return false; + } + this.removeCache(globalDict.getDictCode()); + if (globalDictMapper.deleteById(dictId) == 0) { + return false; + } + GlobalDictItem filter = new GlobalDictItem(); + filter.setDictCode(globalDict.getDictCode()); + globalDictItemService.removeBy(filter); + return true; + } + + @Override + public List getGlobalDictList(GlobalDict filter, String orderBy) { + return globalDictMapper.getGlobalDictList(filter, orderBy); + } + + @Override + public boolean existDictCode(String dictCode) { + return globalDictMapper.selectCountByQuery(new QueryWrapper().eq(GlobalDict::getDictCode, dictCode)) > 0; + } + + @Override + public boolean existDictItemFromCache(String dictCode, Serializable itemId) { + return CollUtil.isNotEmpty(this.getGlobalDictItemListFromCache(dictCode, CollUtil.newHashSet(itemId))); + } + + @Override + public List getGlobalDictItemListFromCache(String dictCode, Set itemIds) { + if (CollUtil.isNotEmpty(itemIds) && !(itemIds.iterator().next() instanceof String)) { + itemIds = itemIds.stream().map(Object::toString).collect(Collectors.toSet()); + } + List dataList; + RMap cachedMap = + redissonClient.getMap(RedisKeyUtil.makeGlobalDictKey(dictCode)); + if (cachedMap.isExists()) { + Map dataMap = + CollUtil.isEmpty(itemIds) ? cachedMap.readAllMap() : cachedMap.getAll(itemIds); + dataList = dataMap.values().stream() + .map(c -> JSON.parseObject(c, GlobalDictItem.class)).collect(Collectors.toList()); + dataList.sort(Comparator.comparingInt(GlobalDictItem::getShowOrder)); + } else { + dataList = globalDictItemService.getGlobalDictItemListByDictCode(dictCode); + this.putCache(dictCode, dataList); + if (CollUtil.isNotEmpty(itemIds)) { + Set tmpItemIds = itemIds; + dataList = dataList.stream() + .filter(c -> tmpItemIds.contains(c.getItemId())).collect(Collectors.toList()); + } + } + return dataList; + } + + @Override + public Map getGlobalDictItemDictMapFromCache(String dictCode, Set itemIds) { + List dataList = this.getGlobalDictItemListFromCache(dictCode, itemIds); + return dataList.stream().collect(Collectors.toMap(GlobalDictItem::getItemId, GlobalDictItem::getItemName)); + } + + @Override + public void reloadCachedData(String dictCode) { + this.removeCache(dictCode); + List dataList = globalDictItemService.getGlobalDictItemListByDictCode(dictCode); + this.putCache(dictCode, dataList); + } + + @Override + public void removeCache(String dictCode) { + if (StrUtil.isNotBlank(dictCode)) { + redissonClient.getMap(RedisKeyUtil.makeGlobalDictKey(dictCode)).delete(); + } + } + + private void putCache(String dictCode, List globalDictItemList) { + if (CollUtil.isNotEmpty(globalDictItemList)) { + Map dataMap = globalDictItemList.stream() + .filter(item -> item.getStatus() == GlobalDictItemStatus.NORMAL) + .collect(Collectors.toMap(GlobalDictItem::getItemId, JSON::toJSONString)); + if (MapUtil.isNotEmpty(dataMap)) { + redissonClient.getMap(RedisKeyUtil.makeGlobalDictKey(dictCode)).putAll(dataMap); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java new file mode 100644 index 00000000..71aed12c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java @@ -0,0 +1,189 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.BooleanUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.TenantGlobalDictItemMapper; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; +import com.orangeforms.common.dict.service.TenantGlobalDictItemService; +import com.orangeforms.common.dict.service.TenantGlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 租户全局字典项目数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.TENANT_COMMON_DATASOURCE_TYPE) +@Slf4j +@Service("tenantGlobalDictItemService") +public class TenantGlobalDictItemServiceImpl + extends BaseService implements TenantGlobalDictItemService { + + @Autowired + private TenantGlobalDictItemMapper tenantGlobalDictItemMapper; + @Autowired + private TenantGlobalDictService tenantGlobalDictService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return tenantGlobalDictItemMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public TenantGlobalDictItem saveNew(TenantGlobalDict dict, TenantGlobalDictItem dictItem) { + tenantGlobalDictService.removeCache(dict); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + dictItem.setTenantId(TokenData.takeFromRequest().getTenantId()); + } + dictItem.setId(idGenerator.nextLongId()); + dictItem.setDeletedFlag(GlobalDeletedFlag.NORMAL); + dictItem.setStatus(GlobalDictItemStatus.NORMAL); + dictItem.setCreateUserId(TokenData.takeFromRequest().getUserId()); + dictItem.setUpdateUserId(dictItem.getCreateUserId()); + dictItem.setCreateTime(new Date()); + dictItem.setUpdateTime(dictItem.getCreateTime()); + tenantGlobalDictItemMapper.insert(dictItem); + return dictItem; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewBatch(List dictItemList) { + if (CollUtil.isEmpty(dictItemList)) { + return; + } + Date now = new Date(); + for (TenantGlobalDictItem dictItem : dictItemList) { + if (dictItem.getId() == null) { + dictItem.setId(idGenerator.nextLongId()); + } + if (dictItem.getCreateUserId() == null) { + dictItem.setCreateUserId(TokenData.takeFromRequest().getUserId()); + } + dictItem.setUpdateUserId(dictItem.getCreateUserId()); + dictItem.setUpdateTime(now); + dictItem.setCreateTime(now); + dictItem.setStatus(GlobalDictItemStatus.NORMAL); + dictItem.setDeletedFlag(GlobalDeletedFlag.NORMAL); + } + tenantGlobalDictItemMapper.insertList(dictItemList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(TenantGlobalDict dict, TenantGlobalDictItem dictItem, TenantGlobalDictItem originalDictItem) { + tenantGlobalDictService.removeCache(dict); + // 该方法不能直接修改字典状态,更不会修改tenantId。 + dictItem.setStatus(originalDictItem.getStatus()); + dictItem.setTenantId(originalDictItem.getTenantId()); + dictItem.setCreateUserId(originalDictItem.getCreateUserId()); + dictItem.setCreateTime(originalDictItem.getCreateTime()); + dictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + dictItem.setUpdateTime(new Date()); + return tenantGlobalDictItemMapper.update(dictItem) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateNewCode(String oldCode, String newCode) { + TenantGlobalDictItem dictItem = new TenantGlobalDictItem(); + dictItem.setDictCode(newCode); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, oldCode); + tenantGlobalDictItemMapper.updateByQuery(dictItem, queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateStatus(TenantGlobalDict dict, TenantGlobalDictItem dictItem, Integer status) { + tenantGlobalDictService.removeCache(dict); + dictItem.setStatus(status); + dictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + dictItem.setUpdateTime(new Date()); + tenantGlobalDictItemMapper.update(dictItem); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(TenantGlobalDict dict, TenantGlobalDictItem dictItem) { + tenantGlobalDictService.removeCache(dict); + return this.removeById(dictItem.getId()); + } + + @Override + public boolean existDictCodeAndItemId(TenantGlobalDict dict, Serializable itemId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dict.getDictCode()); + queryWrapper.eq(TenantGlobalDictItem::getItemId, itemId.toString()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + queryWrapper.eq(TenantGlobalDictItem::getTenantId, TokenData.takeFromRequest().getTenantId()); + } + return tenantGlobalDictItemMapper.selectCountByQuery(queryWrapper) > 0; + } + + @Override + public boolean existDictCode(String dictCode) { + return tenantGlobalDictItemMapper.selectCountByQuery( + new QueryWrapper().eq(TenantGlobalDictItem::getDictCode, dictCode)) > 0; + } + + @Override + public TenantGlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dictCode); + queryWrapper.eq(TenantGlobalDictItem::getItemId, itemId.toString()); + return tenantGlobalDictItemMapper.selectOneByQuery(queryWrapper); + } + + @Override + public List getGlobalDictItemList(TenantGlobalDictItem filter, String orderBy) { + QueryWrapper queryWrapper = filter == null ? QueryWrapper.create() : QueryWrapper.create(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } else { + queryWrapper.orderBy(TenantGlobalDictItem::getShowOrder, true); + } + return tenantGlobalDictItemMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getGlobalDictItemList(TenantGlobalDict dict) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + queryWrapper.eq(TenantGlobalDictItem::getTenantId, TokenData.takeFromRequest().getTenantId()); + } + queryWrapper.orderBy(TenantGlobalDictItem::getShowOrder, true); + return tenantGlobalDictItemMapper.selectListByQuery(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java new file mode 100644 index 00000000..cbf7ea20 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java @@ -0,0 +1,302 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.TenantGlobalDictMapper; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; +import com.orangeforms.common.dict.service.TenantGlobalDictItemService; +import com.orangeforms.common.dict.service.TenantGlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 租户全局字典数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.TENANT_COMMON_DATASOURCE_TYPE) +@Slf4j +@Service("tenantGlobalDictService") +public class TenantGlobalDictServiceImpl + extends BaseService implements TenantGlobalDictService { + + @Autowired + private TenantGlobalDictMapper tenantGlobalDictMapper; + @Autowired + private TenantGlobalDictItemService tenantGlobalDictItemService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return tenantGlobalDictMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public TenantGlobalDict saveNew(TenantGlobalDict dict, Set tenantIdSet) { + String initialData = dict.getInitialData(); + dict.setDictId(idGenerator.nextLongId()); + dict.setDeletedFlag(GlobalDeletedFlag.NORMAL); + dict.setCreateUserId(TokenData.takeFromRequest().getUserId()); + dict.setUpdateUserId(dict.getCreateUserId()); + dict.setCreateTime(new Date()); + dict.setUpdateTime(dict.getCreateTime()); + if (BooleanUtil.isTrue(dict.getTenantCommon())) { + dict.setInitialData(null); + } + tenantGlobalDictMapper.insert(dict); + List dictItemList = null; + if (StrUtil.isNotBlank(initialData)) { + dictItemList = JSONArray.parseArray(initialData, TenantGlobalDictItem.class); + dictItemList.forEach(dictItem -> { + dictItem.setDictCode(dict.getDictCode()); + dictItem.setCreateUserId(dict.getCreateUserId()); + }); + } + if (BooleanUtil.isTrue(dict.getTenantCommon())) { + tenantGlobalDictItemService.saveNewBatch(dictItemList); + } else { + if (CollUtil.isEmpty(tenantIdSet) || dictItemList == null) { + return dict; + } + for (Long tenantId : tenantIdSet) { + dictItemList.forEach(dictItem -> { + dictItem.setId(idGenerator.nextLongId()); + dictItem.setTenantId(tenantId); + }); + tenantGlobalDictItemService.saveNewBatch(dictItemList); + } + } + return dict; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(TenantGlobalDict dict, TenantGlobalDict originalDict) { + this.removeGlobalDictAllCache(originalDict); + dict.setCreateUserId(originalDict.getCreateUserId()); + dict.setCreateTime(originalDict.getCreateTime()); + dict.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + dict.setUpdateTime(new Date()); + if (tenantGlobalDictMapper.update(dict) != 1) { + return false; + } + if (!StrUtil.equals(dict.getDictCode(), originalDict.getDictCode())) { + tenantGlobalDictItemService.updateNewCode(originalDict.getDictCode(), dict.getDictCode()); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dictId) { + TenantGlobalDict dict = this.getById(dictId); + if (dict == null) { + return false; + } + this.removeGlobalDictAllCache(dict); + if (tenantGlobalDictMapper.deleteById(dictId) == 0) { + return false; + } + TenantGlobalDictItem filter = new TenantGlobalDictItem(); + filter.setDictCode(dict.getDictCode()); + tenantGlobalDictItemService.removeBy(filter); + return true; + } + + @Override + public List getGlobalDictList(TenantGlobalDict filter, String orderBy) { + QueryWrapper queryWrapper = filter == null ? QueryWrapper.create() : QueryWrapper.create(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return tenantGlobalDictMapper.selectListByQuery(queryWrapper); + } + + @Override + public boolean existDictCode(String dictCode) { + return tenantGlobalDictMapper.selectCountByQuery( + new QueryWrapper().eq(TenantGlobalDict::getDictCode, dictCode)) > 0; + } + + @Override + public TenantGlobalDict getTenantGlobalDictByDictCode(String dictCode) { + return tenantGlobalDictMapper.selectOneByQuery(new QueryWrapper().eq(TenantGlobalDict::getDictCode, dictCode)); + } + + @Override + public TenantGlobalDict getTenantGlobalDictFromCache(String dictCode) { + String key = RedisKeyUtil.makeGlobalDictOnlyKey(dictCode); + RBucket bucket = redissonClient.getBucket(key); + if (bucket.isExists()) { + return JSON.parseObject(bucket.get(), TenantGlobalDict.class); + } + TenantGlobalDict dict = this.getTenantGlobalDictByDictCode(dictCode); + if (dict != null) { + bucket.set(JSON.toJSONString(dict)); + } + return dict; + } + + @Override + public List getGlobalDictItemListFromCache(TenantGlobalDict dict, Set itemIds) { + if (CollUtil.isNotEmpty(itemIds) && !(itemIds.iterator().next() instanceof String)) { + itemIds = itemIds.stream().map(Object::toString).collect(Collectors.toSet()); + } + String key = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + key = this.appendTenantSuffix(key); + } + List dataList; + RMap cachedMap = redissonClient.getMap(key); + if (cachedMap.isExists()) { + Map dataMap = + CollUtil.isEmpty(itemIds) ? cachedMap.readAllMap() : cachedMap.getAll(itemIds); + dataList = dataMap.values().stream() + .map(c -> JSON.parseObject(c, TenantGlobalDictItem.class)).collect(Collectors.toList()); + dataList.sort(Comparator.comparingInt(TenantGlobalDictItem::getShowOrder)); + } else { + dataList = tenantGlobalDictItemService.getGlobalDictItemList(dict); + this.putCache(dict, dataList); + if (CollUtil.isNotEmpty(itemIds)) { + Set tmpItemIds = itemIds; + dataList = dataList.stream() + .filter(c -> tmpItemIds.contains(c.getItemId())).collect(Collectors.toList()); + } + } + return dataList; + } + + @Override + public Map getGlobalDictItemDictMapFromCache( + TenantGlobalDict dict, Set itemIds) { + List dataList = this.getGlobalDictItemListFromCache(dict, itemIds); + return dataList.stream() + .collect(Collectors.toMap(TenantGlobalDictItem::getItemId, TenantGlobalDictItem::getItemName)); + } + + @Override + public void reloadCachedData(TenantGlobalDict dict) { + this.removeCache(dict); + List dataList = tenantGlobalDictItemService.getGlobalDictItemList(dict); + this.putCache(dict, dataList); + } + + @Override + public void reloadAllTenantCachedData(TenantGlobalDict dict) { + if (StrUtil.isBlank(dict.getDictCode())) { + return; + } + String dictCodeKey = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + redissonClient.getKeys().deleteByPattern(dictCodeKey + "*"); + TenantGlobalDictItem filter = new TenantGlobalDictItem(); + filter.setDictCode(dict.getDictCode()); + List dictItemList = + tenantGlobalDictItemService.getGlobalDictItemList(filter, null); + if (CollUtil.isEmpty(dictItemList)) { + return; + } + Map> dictItemMap = + dictItemList.stream().collect(Collectors.groupingBy(TenantGlobalDictItem::getTenantId)); + for (Map.Entry> entry : dictItemMap.entrySet()) { + String key = dictCodeKey + "-" + entry.getKey(); + Map dataMap = entry.getValue().stream() + .collect(Collectors.toMap(TenantGlobalDictItem::getItemId, JSON::toJSONString)); + RMap cachedMap = redissonClient.getMap(key); + cachedMap.putAll(dataMap); + cachedMap.expire(1, TimeUnit.DAYS); + } + } + + @Override + public void removeCache(TenantGlobalDict dict) { + if (StrUtil.isBlank(dict.getDictCode())) { + return; + } + String key = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + key = this.appendTenantSuffix(key); + } + redissonClient.getMap(key).delete(); + } + + @Override + public boolean existDictItemFromCache(String dictCode, Serializable itemId) { + TenantGlobalDict tenantGlobalDict = this.getTenantGlobalDictFromCache(dictCode); + return CollUtil.isNotEmpty(this.getGlobalDictItemListFromCache(tenantGlobalDict, CollUtil.newHashSet(itemId))); + } + + private void putCache(TenantGlobalDict dict, List dictItemList) { + if (CollUtil.isEmpty(dictItemList)) { + return; + } + String key = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + key = this.appendTenantSuffix(key); + } + Map dataMap = dictItemList.stream() + .filter(item -> item.getStatus() == GlobalDictItemStatus.NORMAL) + .collect(Collectors.toMap(TenantGlobalDictItem::getItemId, JSON::toJSONString)); + if (MapUtil.isNotEmpty(dataMap)) { + RMap cachedMap = redissonClient.getMap(key); + cachedMap.putAll(dataMap); + cachedMap.expire(1, TimeUnit.DAYS); + } + } + + private String appendTenantSuffix(String key) { + return key + "-" + TokenData.takeFromRequest().getTenantId(); + } + + private void removeGlobalDictAllCache(TenantGlobalDict dict) { + String dictCode = dict.getDictCode(); + if (StrUtil.isBlank(dictCode)) { + return; + } + String key = RedisKeyUtil.makeGlobalDictOnlyKey(dictCode); + redissonClient.getBucket(key).delete(); + key = RedisKeyUtil.makeGlobalDictKey(dictCode); + if (BooleanUtil.isTrue(dict.getTenantCommon())) { + redissonClient.getMap(key).delete(); + } else { + redissonClient.getKeys().deleteByPatternAsync(key + "*"); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java new file mode 100644 index 00000000..05e308ef --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java @@ -0,0 +1,89 @@ +package com.orangeforms.common.dict.util; + +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.dict.dto.GlobalDictDto; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.dict.vo.GlobalDictVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 全局编码字典操作的通用帮助对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class GlobalDictOperationHelper { + + @Autowired + private GlobalDictService globalDictService; + + /** + * 获取全部编码字典列表。 + * + * @param globalDictDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 字典的数据列表。 + */ + public ResponseResult> listAllGlobalDict( + GlobalDictDto globalDictDtoFilter, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + GlobalDict filter = MyModelUtil.copyTo(globalDictDtoFilter, GlobalDict.class); + List dictList = globalDictService.getGlobalDictList(filter, null); + List dictVoList = MyModelUtil.copyCollectionTo(dictList, GlobalDictVo.class); + long totalCount = 0L; + if (dictList instanceof Page) { + totalCount = ((Page) dictList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(dictVoList, totalCount)); + } + + public List> toDictDataList(List resultList, String itemIdType) { + return resultList.stream().map(item -> { + Map dataMap = new HashMap<>(4); + Object itemId = item.getItemId(); + if (StrUtil.equals(itemIdType, "Long")) { + itemId = Long.valueOf(item.getItemId()); + } else if (StrUtil.equals(itemIdType, "Integer")) { + itemId = Integer.valueOf(item.getItemId()); + } + dataMap.put(ApplicationConstant.DICT_ID, itemId); + dataMap.put(ApplicationConstant.DICT_NAME, item.getItemName()); + dataMap.put("showOrder", item.getShowOrder()); + dataMap.put("status", item.getStatus()); + return dataMap; + }).collect(Collectors.toList()); + } + + public List> toDictDataList2(List resultList) { + return resultList.stream().map(item -> { + Map dataMap = new HashMap<>(5); + dataMap.put(ApplicationConstant.DICT_ID, item.getId()); + dataMap.put("itemId", item.getItemId()); + dataMap.put(ApplicationConstant.DICT_NAME, item.getItemName()); + dataMap.put("showOrder", item.getShowOrder()); + dataMap.put("status", item.getStatus()); + return dataMap; + }).collect(Collectors.toList()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java new file mode 100644 index 00000000..cbf07bd4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典项目Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典项目Vo") +@Data +public class GlobalDictItemVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long id; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + private String dictCode; + + /** + * 字典数据项Id。 + */ + @Schema(description = "字典数据项Id") + private String itemId; + + /** + * 字典数据项名称。 + */ + @Schema(description = "字典数据项名称") + private String itemName; + + /** + * 显示顺序(数值越小越靠前)。 + */ + @Schema(description = "显示顺序") + private Integer showOrder; + + /** + * 字典状态。具体值引用DictItemStatus常量类。 + */ + @Schema(description = "字典状态") + private Integer status; + + /** + * 创建用户Id。 + */ + @Schema(description = "创建用户Id") + private Long createUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建用户名。 + */ + @Schema(description = "创建用户名") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java new file mode 100644 index 00000000..f77a2581 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典Vo") +@Data +public class GlobalDictVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long dictId; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + private String dictCode; + + /** + * 字典中文名称。 + */ + @Schema(description = "字典中文名称") + private String dictName; + + /** + * 创建用户Id。 + */ + @Schema(description = "创建用户Id") + private Long createUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建用户名。 + */ + @Schema(description = "创建用户名") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java new file mode 100644 index 00000000..967b561d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典项目Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典项目Vo") +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantGlobalDictItemVo extends GlobalDictItemVo { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java new file mode 100644 index 00000000..94ac38fc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典Vo") +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantGlobalDictVo extends GlobalDictVo { + + /** + * 是否为所有租户的通用字典。 + */ + @Schema(description = "是否为所有租户的通用字典") + private Boolean tenantCommon; + + /** + * 租户的非公用字典的初始化字典数据。 + */ + @Schema(description = "租户的非公用字典的初始化字典数据") + private String initialData; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-ext/pom.xml new file mode 100644 index 00000000..f34963db --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/pom.xml @@ -0,0 +1,21 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-ext + + + + com.orangeforms + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java new file mode 100644 index 00000000..81673674 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.ext.base; + +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; + +import java.util.List; +import java.util.Map; + +/** + * 业务组件获取数据的数据源接口。 + * 如果业务服务集成了common-ext组件,可以通过实现该接口的方式,为BizWidgetController访问提供数据。 + * 对于没有集成common-ext组件的服务,可以通过http方式,为BizWidgetController访问提供数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface BizWidgetDatasource { + + /** + * 获取指定通用业务组件的数据。 + * + * @param widgetType 业务组件类型。 + * @param filter 过滤参数。不同的数据源参数不同。这里我们以键值对的方式传递。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 查询后的分页数据列表。 + */ + MyPageData> getDataList( + String widgetType, Map filter, MyOrderParam orderParam, MyPageParam pageParam); + + /** + * 获取指定主键Id的数据对象。 + * + * @param widgetType 业务组件类型。 + * @param fieldName 字段名,如果为空,则使用主键字段名。 + * @param fieldValues 字段值集合。 + * @return 指定主键Id的数据对象。 + */ + List> getDataListWithInList(String widgetType, String fieldName, List fieldValues); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java new file mode 100644 index 00000000..41180d8c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.ext.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-ext通用扩展模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({CommonExtProperties.class}) +public class CommonExtAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java new file mode 100644 index 00000000..7aeb2c23 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java @@ -0,0 +1,76 @@ +package com.orangeforms.common.ext.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.Data; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * common-ext配置属性类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-ext") +public class CommonExtProperties implements InitializingBean { + + /** + * 上传存储类型。具体值可参考枚举 UploadStoreTypeEnum。默认0为本地存储。 + */ + @Value("${common-ext.uploadStoreType:0}") + private Integer uploadStoreType; + + /** + * 仅当uploadStoreType等于0的时候,该配置值生效。 + */ + @Value("${common-ext.uploadFileBaseDir:./zz-resource/upload-files/commonext}") + private String uploadFileBaseDir; + + private List apps; + + private Map applicationMap; + + @Override + public void afterPropertiesSet() throws Exception { + if (CollUtil.isEmpty(apps)) { + applicationMap = new HashMap<>(1); + } else { + applicationMap = apps.stream().collect(Collectors.toMap(AppProperties::getAppCode, c -> c)); + } + } + + @Data + public static class AppProperties { + /** + * 应用编码。 + */ + private String appCode; + /** + * 通用业务组件数据源属性列表。 + */ + private List bizWidgetDatasources; + } + + @Data + public static class BizWidgetDatasourceProperties { + /** + * 通用业务组件的数据源类型。多个类型之间逗号分隔,如:upms_user,upms_dept。 + */ + private String types; + /** + * 列表数据接口地址。格式为完整的url,如:http://xxxxx + */ + private String listUrl; + /** + * 详情数据接口地址。格式为完整的url,如:http://xxxxx + */ + private String viewUrl; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java new file mode 100644 index 00000000..5d3b4ae6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.ext.constant; + +/** + * 业务组件数据源类型常量类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class BizWidgetDatasourceType { + + /** + * 通用用户组件数据源类型。 + */ + public static final String UPMS_USER_TYPE = "upms_user"; + + /** + * 通用部门组件数据源类型。 + */ + public static final String UPMS_DEPT_TYPE = "upms_dept"; + + /** + * 通用角色组件数据源类型。 + */ + public static final String UPMS_ROLE_TYPE = "upms_role"; + + /** + * 通用岗位组件数据源类型。 + */ + public static final String UPMS_POST_TYPE = "upms_post"; + + /** + * 通用部门岗位组件数据源类型。 + */ + public static final String UPMS_DEPT_POST_TYPE = "upms_dept_post"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private BizWidgetDatasourceType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java new file mode 100644 index 00000000..021ac5e1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.ext.controller; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.core.annotation.MyRequestBody; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 业务组件获取数据的访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@RestController +@RequestMapping("${common-ext.urlPrefix}/bizwidget") +public class BizWidgetController { + + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + @PostMapping("/list") + public ResponseResult>> list( + @MyRequestBody(required = true) String widgetType, + @MyRequestBody JSONObject filter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + String appCode = TokenData.takeFromRequest().getAppCode(); + MyPageData> pageData = + bizWidgetDatasourceExtHelper.getDataList(appCode, widgetType, filter, orderParam, pageParam); + return ResponseResult.success(pageData); + } + + /** + * 查看指定多条数据的详情。 + * + * @param widgetType 组件类型。 + * @param fieldName 字段名,如果为空则默认为主键过滤。 + * @param fieldValues 字段值。多个值之间逗号分割。 + * @return 详情数据。 + */ + @PostMapping("/view") + public ResponseResult>> view( + @MyRequestBody(required = true) String widgetType, + @MyRequestBody String fieldName, + @MyRequestBody(required = true) String fieldValues) { + String appCode = TokenData.takeFromRequest().getAppCode(); + List> dataMapList = + bizWidgetDatasourceExtHelper.getDataListWithInList(appCode, widgetType, fieldName, fieldValues); + return ResponseResult.success(dataMapList); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java new file mode 100644 index 00000000..0d94cc1c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java @@ -0,0 +1,112 @@ +package com.orangeforms.common.ext.controller; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.ext.config.CommonExtProperties; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBinaryStream; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; + +/** + * 扩展工具接口类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@RestController +@RequestMapping("${common-ext.urlPrefix}/util") +public class UtilController { + + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private CommonExtProperties properties; + @Autowired + private RedissonClient redissonClient; + + private static final String IMAGE_DATA_FIELD = "imageData"; + + /** + * 上传图片数据。 + * + * @param uploadFile 上传图片文件。 + */ + @PostMapping("/uploadImage") + public void uploadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + BaseUpDownloader upDownloader = + upDownloaderFactory.get(EnumUtil.getEnumAt(UploadStoreTypeEnum.class, properties.getUploadStoreType())); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + properties.getUploadFileBaseDir(), "CommonExt", IMAGE_DATA_FIELD, true, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + String uploadUri = ContextUtil.getHttpRequest().getRequestURI(); + uploadUri = StrUtil.removeSuffix(uploadUri, "/"); + uploadUri = StrUtil.removeSuffix(uploadUri, "/uploadImage"); + responseInfo.setDownloadUri(uploadUri + "/downloadImage"); + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + /** + * 下载图片数据。 + * + * @param filename 文件名。 + * @param response Http 应答对象。 + */ + @GetMapping("/downloadImage") + public void downloadImage(@RequestParam String filename, HttpServletResponse response) { + try { + BaseUpDownloader upDownloader = + upDownloaderFactory.get(EnumUtil.getEnumAt(UploadStoreTypeEnum.class, properties.getUploadStoreType())); + upDownloader.doDownload(properties.getUploadFileBaseDir(), + "CommonExt", IMAGE_DATA_FIELD, filename, true, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + /** + * 下载缓存的会话图片数据。 + * + * @param filename 文件名。 + * @param response Http 应答对象。 + */ + @GetMapping("/downloadSessionImage") + public void downloadSessionImage(@RequestParam String filename, HttpServletResponse response) throws IOException { + TokenData tokenData = TokenData.takeFromRequest(); + String key = tokenData.getSessionId() + filename; + RBinaryStream stream = redissonClient.getBinaryStream(key); + if (!stream.isExists()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "无效的会话缓存图片!")); + } + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + filename); + try (OutputStream os = response.getOutputStream()) { + os.write(stream.getAndDelete()); + } catch (IOException e) { + log.error("Failed to call LocalUpDownloader.doDownload", e); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java new file mode 100644 index 00000000..ba9cef17 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java @@ -0,0 +1,209 @@ +package com.orangeforms.common.ext.util; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.config.CommonExtProperties; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 高级通用业务组件的扩展帮助实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class BizWidgetDatasourceExtHelper { + + @Autowired + private CommonExtProperties properties; + /** + * 全部框架使用橙单框架,同时组件所在模块,如在线表单,报表等和业务服务位于同一服务内是使用。 + */ + private static final String DEFAULT_ORANGE_APP = "__DEFAULT_ORANGE_APP__"; + /** + * Map的数据结构为:Map> + */ + private Map> dataExtractorMap = MapUtil.newHashMap(); + + @PostConstruct + private void laodThirdPartyAppConfig() { + Map appPropertiesMap = properties.getApplicationMap(); + if (MapUtil.isEmpty(appPropertiesMap)) { + return; + } + for (Map.Entry entry : appPropertiesMap.entrySet()) { + String appCode = entry.getKey(); + List datasources = entry.getValue().getBizWidgetDatasources(); + Map m = new HashMap<>(datasources.size()); + for (CommonExtProperties.BizWidgetDatasourceProperties datasource : datasources) { + List types = StrUtil.split(datasource.getTypes(), ","); + DatasourceWrapper w = new DatasourceWrapper(); + w.setListUrl(datasource.getListUrl()); + w.setViewUrl(datasource.getViewUrl()); + for (String type : types) { + m.put(type, w); + } + } + dataExtractorMap.put(appCode, m); + } + } + + /** + * 为默认APP注册基础组件数据源对象。 + * + * @param type 数据源类型。 + * @param datasource 业务通用组件的数据源接口。 + */ + public void registerDatasource(String type, BizWidgetDatasource datasource) { + Assert.notBlank(type); + Assert.notNull(datasource); + Map datasourceWrapperMap = + dataExtractorMap.computeIfAbsent(DEFAULT_ORANGE_APP, k -> new HashMap<>(2)); + datasourceWrapperMap.put(type, new DatasourceWrapper(datasource)); + } + + /** + * 根据过滤条件获取指定通用业务组件的数据列表。 + * + * @param appCode 接入应用编码。如果为空,则使用默认的 DEFAULT_ORANGE_APP。 + * @param type 组件数据源类型。 + * @param filter 过滤参数。不同的数据源参数不同。这里我们以键值对的方式传递。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 查询后的分页数据列表。 + */ + public MyPageData> getDataList( + String appCode, String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (StrUtil.isBlank(type)) { + throw new MyRuntimeException("Argument [types] can't be BLANK"); + } + if (StrUtil.isBlank(appCode)) { + return this.getDataList(type, filter, orderParam, pageParam); + } + DatasourceWrapper wrapper = this.getDatasourceWrapper(appCode, type); + JSONObject body = new JSONObject(); + body.put("type", type); + if (MapUtil.isNotEmpty(filter)) { + body.put("filter", filter); + } + if (orderParam != null) { + body.put("orderParam", orderParam); + } + if (pageParam != null) { + body.put("pageParam", pageParam); + } + String response = this.invokeThirdPartyUrlWithPost(wrapper.getListUrl(), body.toJSONString()); + ResponseResult>> responseResult = + JSON.parseObject(response, new TypeReference>>>() { + }); + if (!responseResult.isSuccess()) { + throw new MyRuntimeException(responseResult.getErrorMessage()); + } + return responseResult.getData(); + } + + /** + * 根据指定字段的集合获取指定通用业务组件的数据对象列表。 + * + * @param appCode 接入应用Id。如果为空,则使用默认的 DEFAULT_ORANGE_APP。 + * @param type 组件数据源类型。 + * @param fieldName 字段名称。 + * @param fieldValues 字段值结合。 + * @return 指定字段数据集合的数据对象列表。 + */ + public List> getDataListWithInList( + String appCode, String type, String fieldName, String fieldValues) { + if (StrUtil.isBlank(fieldValues)) { + throw new MyRuntimeException("Argument [fieldValues] can't be BLANK"); + } + if (StrUtil.isBlank(type)) { + throw new MyRuntimeException("Argument [types] can't be BLANK"); + } + if (StrUtil.isBlank(appCode)) { + return this.getDataListWithInList(type, fieldName, fieldValues); + } + DatasourceWrapper wrapper = this.getDatasourceWrapper(appCode, type); + JSONObject body = new JSONObject(); + body.put("type", type); + if (StrUtil.isNotBlank(fieldName)) { + body.put("fieldName", fieldName); + } + body.put("fieldValues", fieldValues); + String response = this.invokeThirdPartyUrlWithPost(wrapper.getViewUrl(), body.toJSONString()); + ResponseResult>> responseResult = + JSON.parseObject(response, new TypeReference>>>() { + }); + if (!responseResult.isSuccess()) { + throw new MyRuntimeException(responseResult.getErrorMessage()); + } + return responseResult.getData(); + } + + private MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + DatasourceWrapper wrapper = this.getDatasourceWrapper(DEFAULT_ORANGE_APP, type); + return wrapper.getBizWidgetDataSource().getDataList(type, filter, orderParam, pageParam); + } + + private List> getDataListWithInList(String type, String fieldName, String fieldValues) { + DatasourceWrapper wrapper = this.getDatasourceWrapper(DEFAULT_ORANGE_APP, type); + return wrapper.getBizWidgetDataSource().getDataListWithInList(type, fieldName, StrUtil.split(fieldValues, ",")); + } + + private String invokeThirdPartyUrlWithPost(String url, String body) { + String token = TokenData.takeFromRequest().getToken(); + Map headerMap = new HashMap<>(1); + headerMap.put("Authorization", token); + StringBuilder fullUrl = new StringBuilder(128); + fullUrl.append(url).append("?token=").append(token); + HttpResponse httpResponse = HttpUtil.createPost(fullUrl.toString()).body(body).addHeaders(headerMap).execute(); + if (!httpResponse.isOk()) { + String msg = StrFormatter.format( + "Failed to call [{}] with ERROR HTTP Status [{}] and [{}].", + url, httpResponse.getStatus(), httpResponse.body()); + log.error(msg); + throw new MyRuntimeException(msg); + } + return httpResponse.body(); + } + + private DatasourceWrapper getDatasourceWrapper(String appCode, String type) { + Map datasourceWrapperMap = dataExtractorMap.get(appCode); + Assert.notNull(datasourceWrapperMap); + DatasourceWrapper wrapper = datasourceWrapperMap.get(type); + Assert.notNull(wrapper); + return wrapper; + } + + @NoArgsConstructor + @Data + public static class DatasourceWrapper { + private BizWidgetDatasource bizWidgetDataSource; + private String listUrl; + private String viewUrl; + + public DatasourceWrapper(BizWidgetDatasource bizWidgetDataSource) { + this.bizWidgetDataSource = bizWidgetDataSource; + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..fc140409 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.ext.config.CommonExtAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/pom.xml new file mode 100644 index 00000000..9e40544e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-flow-online + 1.0.0 + common-flow-online + jar + + + + com.orangeforms + common-flow + 1.0.0 + + + com.orangeforms + common-online + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java new file mode 100644 index 00000000..07538229 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.online.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-flow-online模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({FlowOnlineProperties.class}) +public class FlowOnlineAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java new file mode 100644 index 00000000..143afba4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.flow.online.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 在线表单工作流模块的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-flow-online") +public class FlowOnlineProperties { + + /** + * 在线表单的URL前缀。 + */ + private String urlPrefix; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java new file mode 100644 index 00000000..cdeb15bb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java @@ -0,0 +1,1082 @@ +package com.orangeforms.common.flow.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.util.OnlineOperationHelper; +import com.orangeforms.common.flow.online.service.FlowOnlineOperationService; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.util.FlowOperationHelper; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.dto.FlowTaskCommentDto; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.model.constant.FlowMessageType; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.vo.*; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 工作流在线表单流程操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流在线表单流程操作接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowOnlineOperation") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowOnlineOperationController { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowOperationHelper flowOperationHelper; + @Autowired + private FlowOnlineOperationService flowOnlineOperationService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowMessageService flowMessageService; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineOperationHelper onlineOperationHelper; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private SessionCacheHelper sessionCacheHelper; + + private static final String ONE_TO_MANY_VAR_SUFFIX = "List"; + + /** + * 根据指定流程的主版本,发起一个流程实例,同时作为第一个任务节点的执行人,执行第一个用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * 注:流程设计页面的"启动"按钮,调用该接口可以启动任何流程用于流程配置后的测试验证。 + * + * @param processDefinitionKey 流程定义标识。 + * @param flowTaskCommentDto 审批意见。 + * @param taskVariableData 流程任务变量数据。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @param copyData 传阅数据,格式为type和id,type的值参考FlowConstant中的常量值。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.START_FLOW) + @PostMapping("/startPreview") + public ResponseResult startPreview( + @MyRequestBody(required = true) String processDefinitionKey, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto, + @MyRequestBody JSONObject taskVariableData, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData, + @MyRequestBody JSONObject copyData) { + return this.startAndTake( + processDefinitionKey, flowTaskCommentDto, taskVariableData, masterData, slaveData, copyData); + } + + /** + * 根据指定流程的主版本,发起一个流程实例,同时作为第一个任务节点的执行人,执行第一个用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processDefinitionKey 流程定义标识。 + * @param flowTaskCommentDto 审批意见。 + * @param taskVariableData 流程任务变量数据。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @param copyData 传阅数据,格式为type和id,type的值参考FlowConstant中的常量值。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.START_FLOW) + @PostMapping("/startAndTakeUserTask/{processDefinitionKey}") + public ResponseResult startAndTakeUserTask( + @PathVariable("processDefinitionKey") String processDefinitionKey, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto, + @MyRequestBody JSONObject taskVariableData, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData, + @MyRequestBody JSONObject copyData) { + return this.startAndTake( + processDefinitionKey, flowTaskCommentDto, taskVariableData, masterData, slaveData, copyData); + } + + /** + * 启动流程并创建工单,同时将当前录入的数据存入草稿。 + * + * @param processDefinitionKey 流程定义标识。 + * @param processInstanceId 流程实例Id。第一次保存时,该值为null。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @return 应答结果对象,草稿的待办任务对象。 + */ + @DisableDataFilter + @SaTokenDenyAuth + @PostMapping("/startAndSaveDraft/{processDefinitionKey}") + public ResponseResult startAndSaveDraft( + @PathVariable("processDefinitionKey") String processDefinitionKey, + @MyRequestBody String processInstanceId, + @MyRequestBody JSONObject masterData, + @MyRequestBody JSONObject slaveData) { + String errorMessage; + if (MapUtil.isEmpty(masterData) && MapUtil.isEmpty(slaveData)) { + errorMessage = "数据验证失败,业务数据不能全部为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult> verifyResult = + this.verifyAndGetFlowEntryPublishAndDatasource(processDefinitionKey, true); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntryPublish flowEntryPublish = verifyResult.getData().getFirst(); + OnlineTable masterTable = verifyResult.getData().getSecond().getMasterTable(); + // 自动填充创建人数据。 + for (OnlineColumn column : masterTable.getColumnMap().values()) { + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.CREATE_USER_ID)) { + masterData.put(column.getColumnName(), TokenData.takeFromRequest().getUserId()); + } else if (ObjectUtil.equals(column.getFieldKind(), FieldKind.CREATE_DEPT_ID)) { + masterData.put(column.getColumnName(), TokenData.takeFromRequest().getDeptId()); + } + } + FlowWorkOrder flowWorkOrder; + if (processInstanceId == null) { + flowWorkOrder = flowOnlineOperationService.saveNewDraftAndStartProcess( + flowEntryPublish.getProcessDefinitionId(), masterTable.getTableId(), masterData, slaveData); + } else { + ResponseResult flowWorkOrderResult = + flowOperationHelper.verifyAndGetFlowWorkOrderWithDraft(processDefinitionKey, processInstanceId); + if (!flowWorkOrderResult.isSuccess()) { + return ResponseResult.errorFrom(flowWorkOrderResult); + } + flowWorkOrder = flowWorkOrderResult.getData(); + flowWorkOrderService.updateDraft(flowWorkOrderResult.getData().getWorkOrderId(), + JSON.toJSONString(masterData), JSON.toJSONString(slaveData)); + } + List taskList = flowApiService.getProcessInstanceActiveTaskList(flowWorkOrder.getProcessInstanceId()); + List flowTaskVoList = flowApiService.convertToFlowTaskList(taskList); + return ResponseResult.success(flowTaskVoList.get(0)); + } + + /** + * 提交流程的用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskCommentDto 流程审批数据。 + * @param taskVariableData 流程任务变量数据。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @param copyData 传阅数据,格式为type和id,type的值参考FlowConstant中的常量值。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.SUBMIT_TASK) + @PostMapping("/submitUserTask") + public ResponseResult submitUserTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto, + @MyRequestBody JSONObject taskVariableData, + @MyRequestBody JSONObject masterData, + @MyRequestBody JSONObject slaveData, + @MyRequestBody JSONObject copyData) { + String errorMessage; + // 验证流程任务的合法性。 + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + CallResult assigneeVerifyResult = flowApiService.verifyAssigneeOrCandidateAndClaim(task); + if (!assigneeVerifyResult.isSuccess()) { + return ResponseResult.errorFrom(assigneeVerifyResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + String dataId = instance.getBusinessKey(); + // 这里把传阅数据放到任务变量中,是为了避免给流程数据操作方法增加额外的方法调用参数。 + if (MapUtil.isNotEmpty(copyData)) { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.COPY_DATA_KEY, copyData); + } + FlowTaskComment flowTaskComment = BeanUtil.copyProperties(flowTaskCommentDto, FlowTaskComment.class); + if (StrUtil.isBlank(dataId)) { + return this.submitNewTask(processInstanceId, taskId, + flowTaskComment, taskVariableData, datasource, masterData, slaveData); + } + try { + if (StrUtil.equals(flowTaskComment.getApprovalType(), FlowApprovalType.TRANSFER) + && StrUtil.isBlank(flowTaskComment.getDelegateAssignee())) { + errorMessage = "数据验证失败,加签或转办任务指派人不能为空!!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + flowOnlineOperationService.updateAndTakeTask( + task, flowTaskComment, taskVariableData, datasource, masterData, dataId, slaveDataListResult.getData()); + } catch (FlowOperationException e) { + log.error("Failed to call [FlowOnlineOperationService.updateAndTakeTask]", e); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, e.getMessage()); + } + return ResponseResult.success(); + } + + /** + * 查看指定流程实例的草稿数据。 + * NOTE: 白名单接口。 + * + * @param processDefinitionKey 流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @return 流程实例的草稿数据。 + */ + @DisableDataFilter + @GetMapping("/viewDraftData") + public ResponseResult viewDraftData( + @RequestParam String processDefinitionKey, @RequestParam String processInstanceId) { + String errorMessage; + ResponseResult flowWorkOrderResult = + flowOperationHelper.verifyAndGetFlowWorkOrderWithDraft(processDefinitionKey, processInstanceId); + if (!flowWorkOrderResult.isSuccess()) { + return ResponseResult.errorFrom(flowWorkOrderResult); + } + FlowWorkOrder flowWorkOrder = flowWorkOrderResult.getData(); + if (flowWorkOrder.getOnlineTableId() == null) { + errorMessage = "数据验证失败,当前工单不是在线表单工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowWorkOrderExt flowWorkOrderExt = + flowWorkOrderService.getFlowWorkOrderExtByWorkOrderId(flowWorkOrder.getWorkOrderId()); + if (StrUtil.isBlank(flowWorkOrderExt.getDraftData())) { + return ResponseResult.success(null); + } + Long tableId = flowWorkOrder.getOnlineTableId(); + OnlineTable masterTable = onlineTableService.getOnlineTableFromCache(tableId); + JSONObject draftData = JSON.parseObject(flowWorkOrderExt.getDraftData()); + JSONObject masterData = draftData.getJSONObject(FlowConstant.MASTER_DATA_KEY); + JSONObject slaveData = draftData.getJSONObject(FlowConstant.SLAVE_DATA_KEY); + OnlineDatasource datasource = + onlineDatasourceService.getOnlineDatasourceByMasterTableId(tableId); + List slaveRelationList = null; + if (slaveData != null) { + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + slaveRelationList = relationListResult.getData(); + } + datasource.setMasterTable(masterTable); + JSONObject jsonData = this.buildDraftData(datasource, masterData, slaveRelationList, slaveData); + return ResponseResult.success(jsonData); + } + + /** + * 获取当前流程实例的详情数据。包括主表数据、一对一从表数据、一对多从表数据列表等。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processInstanceId 当前运行时的流程实例Id。 + * @param taskId 流程任务Id。 + * @return 当前流程实例的详情数据。 + */ + @DisableDataFilter + @GetMapping("/viewUserTask") + public ResponseResult viewUserTask( + @RequestParam String processInstanceId, @RequestParam String taskId) { + // 验证流程任务的合法性。 + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + // 如果业务主数据为空,则直接返回。 + if (StrUtil.isBlank(instance.getBusinessKey())) { + return ResponseResult.success(null); + } + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceResult.getData().getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + JSONObject jsonData = this.buildUserTaskData( + instance.getBusinessKey(), datasourceResult.getData(), relationListResult.getData()); + return ResponseResult.success(jsonData); + } + + /** + * 获取已经结束的流程实例的详情数据。包括主表数据、一对一从表数据、一对多从表数据列表等。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processInstanceId 历史流程实例Id。 + * @param taskId 历史任务Id。如果该值为null,仅有发起人可以查看当前流程数据,否则只有任务的指派人才能查看。 + * @return 历史流程实例的详情数据。 + */ + @DisableDataFilter + @GetMapping("/viewHistoricProcessInstance") + public ResponseResult viewHistoricProcessInstance( + @RequestParam String processInstanceId, @RequestParam(required = false) String taskId) { + // 验证流程实例的合法性。 + ResponseResult verifyResult = + flowOperationHelper.verifyAndGetHistoricProcessInstance(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + HistoricProcessInstance instance = verifyResult.getData(); + if (StrUtil.isBlank(instance.getBusinessKey())) { + // 对于没有提交过任何用户任务的场景,可直接返回空数据。 + return ResponseResult.success(new JSONObject()); + } + FlowEntryPublish flowEntryPublish = + flowEntryService.getFlowEntryPublishList(CollUtil.newHashSet(instance.getProcessDefinitionId())).get(0); + TaskInfoVo taskInfoVo = JSON.parseObject(flowEntryPublish.getInitTaskInfo(), TaskInfoVo.class); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfoVo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceResult.getData().getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + JSONObject jsonData = this.buildUserTaskData( + instance.getBusinessKey(), datasourceResult.getData(), relationListResult.getData()); + return ResponseResult.success(jsonData); + } + + /** + * 根据消息Id,获取流程Id关联的业务数据。 + * NOTE:白名单接口。f + * + * @param messageId 抄送消息Id。 + * @return 抄送消息关联的流程实例业务数据。 + */ + @DisableDataFilter + @GetMapping("/viewCopyBusinessData") + public ResponseResult viewCopyBusinessData(@RequestParam Long messageId) { + String errorMessage; + // 验证流程任务的合法性。 + FlowMessage flowMessage = flowMessageService.getById(messageId); + if (flowMessage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (flowMessage.getMessageType() != FlowMessageType.COPY_TYPE) { + errorMessage = "数据验证失败,当前消息不是抄送类型消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowMessage.getOnlineFormData() == null || !flowMessage.getOnlineFormData()) { + errorMessage = "数据验证失败,当前消息为静态路由表单数据,不能通过该接口获取!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowMessageService.isCandidateIdentityOnMessage(messageId)) { + errorMessage = "数据验证失败,当前用户没有权限访问该消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + HistoricProcessInstance instance = + flowApiService.getHistoricProcessInstance(flowMessage.getProcessInstanceId()); + // 如果业务主数据为空,则直接返回。 + if (StrUtil.isBlank(instance.getBusinessKey())) { + errorMessage = "数据验证失败,当前消息为所属流程实例没有包含业务主键Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Long formId = Long.valueOf(flowMessage.getBusinessDataShot()); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(formId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + JSONObject jsonData = this.buildUserTaskData( + instance.getBusinessKey(), datasource, relationListResult.getData()); + // 将当前消息更新为已读 + flowMessageService.readCopyTask(messageId); + return ResponseResult.success(jsonData); + } + + /** + * 工作流工单列表。 + * + * @param processDefinitionKey 流程标识名。 + * @param flowWorkOrderDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 查询结果。 + */ + @SaTokenDenyAuth + @PostMapping("/listWorkOrder/{processDefinitionKey}") + public ResponseResult> listWorkOrder( + @PathVariable("processDefinitionKey") String processDefinitionKey, + @MyRequestBody FlowWorkOrderDto flowWorkOrderDtoFilter, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + FlowWorkOrder flowWorkOrderFilter = + flowOperationHelper.makeWorkOrderFilter(flowWorkOrderDtoFilter, processDefinitionKey); + MyOrderParam orderParam = new MyOrderParam(); + orderParam.add(new MyOrderParam.OrderInfo("workOrderId", false, null)); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowWorkOrder.class); + List flowWorkOrderList = + flowWorkOrderService.getFlowWorkOrderList(flowWorkOrderFilter, orderBy); + MyPageData resultData = + MyPageUtil.makeResponseData(flowWorkOrderList, FlowWorkOrderVo.class); + flowOperationHelper.buildWorkOrderApprovalStatus(processDefinitionKey, resultData.getDataList()); + // 根据工单的提交用户名获取用户的显示名称,便于前端显示。 + // 同时这也是一个如何通过插件方法,将loginName映射到showName的示例, + flowWorkOrderService.fillUserShowNameByLoginName(resultData.getDataList()); + // 工单自身的查询中可以受到数据权限的过滤,但是工单集成业务数据时,则无需再对业务数据进行数据权限过滤了。 + GlobalThreadLocal.setDataFilter(false); + ResponseResult responseResult = this.makeWorkOrderTaskInfo(resultData.getDataList()); + if (!responseResult.isSuccess()) { + return ResponseResult.errorFrom(responseResult); + } + return ResponseResult.success(resultData); + } + + /** + * 为数据源主表字段上传文件。 + * + * @param processDefinitionKey 流程引擎流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param uploadFile 上传文件对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/upload") + public void upload( + @RequestParam String processDefinitionKey, + @RequestParam(required = false) String processInstanceId, + @RequestParam(required = false) String taskId, + @RequestParam Long datasourceId, + @RequestParam(required = false) Long relationId, + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + ResponseResult verifyResult = + this.verifyUploadOrDownload(processDefinitionKey, processInstanceId, taskId, datasourceId); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyResult)); + return; + } + ResponseResult verifyTableResult = + this.verifyAndGetOnlineTable(datasourceId, relationId, null, null); + if (!verifyTableResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyTableResult)); + return; + } + onlineOperationHelper.doUpload(verifyTableResult.getData(), fieldName, asImage, uploadFile); + } + + /** + * 下载文件接口。 + * 越权访问限制说明: + * taskId为空,当前用户必须为当前流程的发起人,否则必须为当前任务的指派人或候选人。 + * relationId为空,下载数据为主表字段,否则为关联的从表字段。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processDefinitionKey 流程引擎流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @param dataId 附件所在记录的主键Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param response Http 应答对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/download") + public void download( + @RequestParam String processDefinitionKey, + @RequestParam(required = false) String processInstanceId, + @RequestParam(required = false) String taskId, + @RequestParam Long datasourceId, + @RequestParam(required = false) Long relationId, + @RequestParam(required = false) String dataId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) throws IOException { + ResponseResult verifyResult = + this.verifyUploadOrDownload(processDefinitionKey, processInstanceId, taskId, datasourceId); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyResult)); + return; + } + ResponseResult verifyTableResult = + this.verifyAndGetOnlineTable(datasourceId, relationId, verifyResult.getData(), dataId); + if (!verifyTableResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyTableResult)); + return; + } + onlineOperationHelper.doDownload(verifyTableResult.getData(), dataId, fieldName, filename, asImage, response); + } + + /** + * 获取所有流程对象,同时获取关联的在线表单对象列表。 + * + * @return 查询结果。 + */ + @GetMapping("/listFlowEntryForm") + public ResponseResult> listFlowEntryForm() { + List flowEntryList = flowEntryService.getFlowEntryList(null, null); + List flowEntryVoList = MyModelUtil.copyCollectionTo(flowEntryList, FlowEntryVo.class); + if (CollUtil.isNotEmpty(flowEntryVoList)) { + Set pageIdSet = flowEntryVoList.stream().map(FlowEntryVo::getPageId).collect(Collectors.toSet()); + List formList = onlineFormService.getOnlineFormListByPageIds(pageIdSet); + formList.forEach(f -> f.setWidgetJson(null)); + Map> formMap = + formList.stream().collect(Collectors.groupingBy(OnlineForm::getPageId)); + for (FlowEntryVo flowEntryVo : flowEntryVoList) { + List flowEntryFormList = formMap.get(flowEntryVo.getPageId()); + flowEntryVo.setFormList(MyModelUtil.beanToMapList(flowEntryFormList)); + } + } + return ResponseResult.success(flowEntryVoList); + } + + /** + * 获取在线表单工作流Id所关联的权限数据,包括权限字列表和权限资源列表。 + * 注:该接口仅用于微服务间调用使用,无需对前端开放。 + * + * @param onlineFlowEntryIds 在线表单工作流Id集合。 + * @return 参数中在线表单工作流Id集合所关联的权限数据。 + */ + @GetMapping("/calculatePermData") + public ResponseResult>> calculatePermData(@RequestParam Set onlineFlowEntryIds) { + return ResponseResult.success(flowOnlineOperationService.calculatePermData(onlineFlowEntryIds)); + } + + private ResponseResult startAndTake( + String processDefinitionKey, + FlowTaskCommentDto flowTaskCommentDto, + JSONObject taskVariableData, + JSONObject masterData, + JSONObject slaveData, + JSONObject copyData) { + ResponseResult> verifyResult = + this.verifyAndGetFlowEntryPublishAndDatasource(processDefinitionKey, true); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntryPublish flowEntryPublish = verifyResult.getData().getFirst(); + OnlineDatasource datasource = verifyResult.getData().getSecond(); + OnlineTable masterTable = datasource.getMasterTable(); + // 这里把传阅数据放到任务变量中,是为了避免给流程数据操作方法增加额外的方法调用参数。 + if (MapUtil.isNotEmpty(copyData)) { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.COPY_DATA_KEY, copyData); + } + FlowTaskComment flowTaskComment = BeanUtil.copyProperties(flowTaskCommentDto, FlowTaskComment.class); + // 保存在线表单提交的数据,同时启动流程和自动完成第一个用户任务。 + if (slaveData == null) { + flowOnlineOperationService.saveNewAndStartProcess( + flowEntryPublish.getProcessDefinitionId(), + flowTaskComment, + taskVariableData, + masterTable, + masterData); + } else { + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + flowOnlineOperationService.saveNewAndStartProcess( + flowEntryPublish.getProcessDefinitionId(), + flowTaskComment, + taskVariableData, + masterTable, + masterData, + slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + private ResponseResult verifyAndGetOnlineDatasource(Long formId) { + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + if (CollUtil.isEmpty(formDatasourceList)) { + String errorMessage = "数据验证失败,流程任务绑定的在线表单Id [" + formId + "] 不存在,请修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return onlineOperationHelper.verifyAndGetDatasource(formDatasourceList.get(0).getDatasourceId()); + } + + private ResponseResult> verifyAndGetFlowEntryPublishAndDatasource( + String processDefinitionKey, boolean checkStarter) { + String errorMessage; + // 1. 验证流程数据的合法性。 + ResponseResult flowEntryResult = flowOperationHelper.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + // 2. 验证流程一个用户任务的合法性。 + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + if (BooleanUtil.isFalse(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布对象已被挂起,不能启动新流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult taskInfoResult = + flowOperationHelper.verifyAndGetInitialTaskInfo(flowEntryPublish, checkStarter); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + // 3. 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + return ResponseResult.success(new Tuple2<>(flowEntryPublish, datasourceResult.getData())); + } + + private ResponseResult verifyAndGetOnlineTable( + Long datasourceId, Long relationId, String businessKey, String dataId) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineTable masterTable = datasourceResult.getData().getMasterTable(); + OnlineTable table = masterTable; + ResponseResult relationResult = null; + if (relationId != null) { + relationResult = onlineOperationHelper.verifyAndGetRelation(datasourceId, relationId); + if (!relationResult.isSuccess()) { + return ResponseResult.errorFrom(relationResult); + } + table = relationResult.getData().getSlaveTable(); + } + if (StrUtil.hasBlank(businessKey, dataId)) { + return ResponseResult.success(table); + } + String errorMessage; + // 如果relationId为null,这里就是主表数据。 + if (relationId == null) { + if (!StrUtil.equals(businessKey, dataId)) { + errorMessage = "数据验证失败,参数主键Id与流程主表主键Id不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(table); + } + OnlineDatasourceRelation relation = relationResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + Map dataMap = + onlineOperationService.getMasterData(slaveTable, null, null, dataId); + if (dataMap == null) { + errorMessage = "数据验证失败,从表主键Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineColumn slaveColumn = relation.getSlaveColumn(); + Object relationSlaveDataId = dataMap.get(slaveColumn.getColumnName()); + if (relationSlaveDataId == null) { + errorMessage = "数据验证失败,当前关联的从表字段值为NULL!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + if (BooleanUtil.isTrue(masterColumn.getPrimaryKey()) + && !StrUtil.equals(relationSlaveDataId.toString(), businessKey)) { + errorMessage = "数据验证失败,当前从表主键Id关联的主表Id当前流程的BusinessKey不一致!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Map masterDataMap = + onlineOperationService.getMasterData(masterTable, null, null, businessKey); + if (masterDataMap == null) { + errorMessage = "数据验证失败,主表主键Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Object relationMasterDataId = masterDataMap.get(masterColumn.getColumnName()); + if (relationMasterDataId == null) { + errorMessage = "数据验证失败,当前关联的主表字段值为NULL!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(relationMasterDataId.toString(), relationSlaveDataId.toString())) { + errorMessage = "数据验证失败,当前关联的主表字段值和从表字段值不一致!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(table); + } + + private ResponseResult verifyUploadOrDownload( + String processDefinitionKey, String processInstanceId, String taskId, Long datasourceId) { + if (!StrUtil.isAllBlank(processInstanceId, taskId)) { + ResponseResult verifyResult = + flowOperationHelper.verifyUploadOrDownloadPermission(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(ResponseResult.errorFrom(verifyResult)); + } + } + String errorMessage; + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (flowEntry == null) { + errorMessage = "数据验证失败,指定流程Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String businessKey = null; + if (processInstanceId != null) { + HistoricProcessInstance instance = flowApiService.getHistoricProcessInstance(processInstanceId); + if (!StrUtil.equals(flowEntry.getProcessDefinitionKey(), instance.getProcessDefinitionKey())) { + errorMessage = "数据验证失败,指定流程实例并不属于当前流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + businessKey = instance.getBusinessKey(); + } + List datasourceList = + onlinePageService.getOnlinePageDatasourceListByPageId(flowEntry.getPageId()); + Optional r = datasourceList.stream() + .map(OnlinePageDatasource::getDatasourceId).filter(c -> c.equals(datasourceId)).findFirst(); + if (r.isEmpty()) { + errorMessage = "数据验证失败,当前数据源Id并不属于当前流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(businessKey); + } + + private ResponseResult submitNewTask( + String instanceId, + String taskId, + FlowTaskComment comment, + JSONObject variableData, + OnlineDatasource datasource, + JSONObject masterData, + JSONObject slaveData) { + OnlineTable masterTable = datasource.getMasterTable(); + // 保存在线表单提交的数据,同时启动流程和自动完成第一个用户任务。 + if (slaveData == null) { + flowOnlineOperationService.saveNewAndTakeTask( + instanceId, taskId, comment, variableData, masterTable, masterData); + } else { + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + flowOnlineOperationService.saveNewAndTakeTask( + instanceId, taskId, comment, variableData, masterTable, masterData, slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + private JSONObject buildUserTaskData( + String businessKey, OnlineDatasource datasource, List relationList) { + OnlineTable masterTable = datasource.getMasterTable(); + JSONObject jsonData = new JSONObject(); + List oneToOneRelationList = relationList.stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + Map result = + onlineOperationService.getMasterData(masterTable, oneToOneRelationList, relationList, businessKey); + if (MapUtil.isEmpty(result)) { + return jsonData; + } + jsonData.put(datasource.getVariableName(), result); + List oneToManyRelationList = relationList.stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_MANY)).collect(Collectors.toList()); + if (CollUtil.isEmpty(oneToManyRelationList)) { + return jsonData; + } + for (OnlineDatasourceRelation relation : oneToManyRelationList) { + OnlineFilterDto filterDto = new OnlineFilterDto(); + filterDto.setTableName(relation.getSlaveTable().getTableName()); + OnlineColumn slaveColumn = relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + filterDto.setColumnName(slaveColumn.getColumnName()); + filterDto.setFilterType(FieldFilterType.EQUAL_FILTER); + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + Object columnValue = result.get(masterColumn.getColumnName()); + filterDto.setColumnValue(columnValue); + MyPageData> pageData = onlineOperationService.getSlaveDataList( + relation, CollUtil.newLinkedList(filterDto), null, null); + if (CollUtil.isNotEmpty(pageData.getDataList())) { + result.put(relation.getVariableName() + ONE_TO_MANY_VAR_SUFFIX, pageData.getDataList()); + } + } + return jsonData; + } + + private JSONObject buildDraftData( + OnlineDatasource datasource, + JSONObject masterData, + List relationList, + JSONObject slaveData) { + OnlineTable masterTable = datasource.getMasterTable(); + JSONObject jsonData = new JSONObject(); + JSONObject normalizedMasterData = new JSONObject(); + Map columnNameAndColumnMap = masterTable.getColumnMap() + .values().stream().collect(Collectors.toMap(OnlineColumn::getColumnName, c -> c)); + if (masterData != null) { + for (Map.Entry entry : masterData.entrySet()) { + OnlineColumn column = columnNameAndColumnMap.get(entry.getKey()); + Object v = onlineOperationHelper.convertToTypeValue(column, entry.getValue().toString()); + normalizedMasterData.put(entry.getKey(), v); + } + } + if (slaveData != null && relationList != null) { + Map relationMap = + relationList.stream().collect(Collectors.toMap(OnlineDatasourceRelation::getRelationId, c -> c)); + for (Map.Entry entry : slaveData.entrySet()) { + OnlineDatasourceRelation relation = relationMap.get(Long.valueOf(entry.getKey())); + if (relation != null) { + this.buildRelationDraftData(relation, entry.getValue(), normalizedMasterData); + } + } + } + jsonData.put(datasource.getVariableName(), normalizedMasterData); + return jsonData; + } + + private void buildRelationDraftData(OnlineDatasourceRelation relation, Object value, JSONObject masterData) { + if (relation.getRelationType().equals(RelationType.ONE_TO_ONE)) { + Map slaveColumnNameAndColumnMap = + relation.getSlaveTable().getColumnMap().values() + .stream().collect(Collectors.toMap(OnlineColumn::getColumnName, c -> c)); + JSONObject slaveObject = (JSONObject) value; + JSONObject normalizedSlaveObject = new JSONObject(); + for (Map.Entry entry2 : slaveObject.entrySet()) { + OnlineColumn column = slaveColumnNameAndColumnMap.get(entry2.getKey()); + Object v = onlineOperationHelper.convertToTypeValue(column, entry2.getValue().toString()); + normalizedSlaveObject.put(entry2.getKey(), v); + } + masterData.put(relation.getVariableName(), normalizedSlaveObject); + } else if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + JSONArray slaveArray = (JSONArray) value; + JSONArray normalizedSlaveArray = new JSONArray(); + for (int i = 0; i <= slaveArray.size() - 1; i++) { + JSONObject slaveObject = slaveArray.getJSONObject(i); + JSONObject normalizedSlaveObject = new JSONObject(); + normalizedSlaveObject.putAll(slaveObject); + normalizedSlaveArray.add(normalizedSlaveObject); + } + masterData.put(relation.getVariableName(), normalizedSlaveArray); + } + } + + private ResponseResult makeWorkOrderTaskInfo(List flowWorkOrderVoList) { + if (CollUtil.isEmpty(flowWorkOrderVoList)) { + return ResponseResult.success(); + } + Set definitionIdSet = + flowWorkOrderVoList.stream().map(FlowWorkOrderVo::getProcessDefinitionId).collect(Collectors.toSet()); + List flowEntryPublishList = flowEntryService.getFlowEntryPublishList(definitionIdSet); + Map flowEntryPublishMap = + flowEntryPublishList.stream().collect(Collectors.toMap(FlowEntryPublish::getProcessDefinitionId, c -> c)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + FlowEntryPublish flowEntryPublish = flowEntryPublishMap.get(flowWorkOrderVo.getProcessDefinitionId()); + flowWorkOrderVo.setInitTaskInfo(flowEntryPublish.getInitTaskInfo()); + } + Long tableId = flowWorkOrderVoList.get(0).getOnlineTableId(); + OnlineTable masterTable = onlineTableService.getOnlineTableFromCache(tableId); + ResponseResult responseResult = + this.buildWorkOrderMasterData(flowWorkOrderVoList, masterTable); + if (!responseResult.isSuccess()) { + return ResponseResult.errorFrom(responseResult); + } + responseResult = this.buildWorkOrderDraftData(flowWorkOrderVoList, masterTable); + if (!responseResult.isSuccess()) { + return ResponseResult.errorFrom(responseResult); + } + List unfinishedProcessInstanceIds = flowWorkOrderVoList.stream() + .filter(c -> !c.getFlowStatus().equals(FlowTaskStatus.FINISHED)) + .map(FlowWorkOrderVo::getProcessInstanceId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(unfinishedProcessInstanceIds)) { + return ResponseResult.success(); + } + Map> taskMap = + flowApiService.getTaskListByProcessInstanceIds(unfinishedProcessInstanceIds) + .stream().collect(Collectors.groupingBy(Task::getProcessInstanceId)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + List instanceTaskList = taskMap.get(flowWorkOrderVo.getProcessInstanceId()); + if (instanceTaskList != null) { + JSONArray taskArray = new JSONArray(); + for (Task task : instanceTaskList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("taskId", task.getId()); + jsonObject.put("taskName", task.getName()); + jsonObject.put("taskKey", task.getTaskDefinitionKey()); + jsonObject.put("assignee", task.getAssignee()); + taskArray.add(jsonObject); + } + flowWorkOrderVo.setRuntimeTaskInfoList(taskArray); + } + } + return ResponseResult.success(); + } + + private ResponseResult buildWorkOrderDraftData( + List flowWorkOrderVoList, OnlineTable masterTable) { + List draftWorkOrderList = flowWorkOrderVoList.stream() + .filter(c -> c.getFlowStatus().equals(FlowTaskStatus.DRAFT)).collect(Collectors.toList()); + if (CollUtil.isEmpty(draftWorkOrderList)) { + return ResponseResult.success(); + } + Set workOrderIdSet = draftWorkOrderList.stream() + .map(FlowWorkOrderVo::getWorkOrderId).collect(Collectors.toSet()); + List workOrderExtList = + flowWorkOrderService.getFlowWorkOrderExtByWorkOrderIds(workOrderIdSet); + Map workOrderExtMap = workOrderExtList.stream() + .collect(Collectors.toMap(FlowWorkOrderExt::getWorkOrderId, c -> c)); + for (FlowWorkOrderVo workOrder : draftWorkOrderList) { + FlowWorkOrderExt workOrderExt = workOrderExtMap.get(workOrder.getWorkOrderId()); + if (workOrderExt == null) { + continue; + } + JSONObject draftData = JSON.parseObject(workOrderExt.getDraftData()); + JSONObject masterData = draftData.getJSONObject(FlowConstant.MASTER_DATA_KEY); + JSONObject slaveData = draftData.getJSONObject(FlowConstant.SLAVE_DATA_KEY); + OnlineDatasource datasource = + onlineDatasourceService.getOnlineDatasourceByMasterTableId(masterTable.getTableId()); + List slaveRelationList = null; + if (slaveData != null) { + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), RelationType.ONE_TO_ONE); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + slaveRelationList = relationListResult.getData(); + } + datasource.setMasterTable(masterTable); + JSONObject jsonData = this.buildDraftData(datasource, masterData, slaveRelationList, slaveData); + JSONObject masterAndOneToOneData = jsonData.getJSONObject(datasource.getVariableName()); + if (MapUtil.isNotEmpty(masterAndOneToOneData)) { + List> dataList = new LinkedList<>(); + dataList.add(masterAndOneToOneData); + onlineOperationService.buildDataListWithDict(masterTable, slaveRelationList, dataList); + } + workOrder.setMasterData(masterAndOneToOneData); + } + return ResponseResult.success(); + } + + private ResponseResult buildWorkOrderMasterData( + List flowWorkOrderVoList, OnlineTable masterTable) { + Set businessKeySet = flowWorkOrderVoList.stream() + .map(FlowWorkOrderVo::getBusinessKey) + .filter(Objects::nonNull).collect(Collectors.toSet()); + if (CollUtil.isEmpty(businessKeySet)) { + return ResponseResult.success(); + } + Set convertedBusinessKeySet = + onlineOperationHelper.convertToTypeValue(masterTable.getPrimaryKeyColumn(), businessKeySet); + List filterList = new LinkedList<>(); + OnlineFilterDto filterDto = new OnlineFilterDto(); + filterDto.setTableName(masterTable.getTableName()); + filterDto.setColumnName(masterTable.getPrimaryKeyColumn().getColumnName()); + filterDto.setFilterType(FieldFilterType.IN_LIST_FILTER); + filterDto.setColumnValueList(new HashSet<>(convertedBusinessKeySet)); + filterList.add(filterDto); + TaskInfoVo taskInfoVo = JSON.parseObject(flowWorkOrderVoList.get(0).getInitTaskInfo(), TaskInfoVo.class); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfoVo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), RelationType.ONE_TO_ONE); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + MyPageData> pageData = onlineOperationService.getMasterDataList( + masterTable, relationListResult.getData(), null, filterList, null, null); + List> dataList = pageData.getDataList(); + Map> dataMap = dataList.stream() + .collect(Collectors.toMap(c -> c.get(masterTable.getPrimaryKeyColumn().getColumnName()).toString(), c -> c)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + if (StrUtil.isNotBlank(flowWorkOrderVo.getBusinessKey())) { + Object dataId = onlineOperationHelper.convertToTypeValue( + masterTable.getPrimaryKeyColumn(), flowWorkOrderVo.getBusinessKey()); + Map data = dataMap.get(dataId.toString()); + if (data != null) { + flowWorkOrderVo.setMasterData(data); + } + } + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java new file mode 100644 index 00000000..79b7f412 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java @@ -0,0 +1,136 @@ +package com.orangeforms.common.flow.online.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.FlowTaskComment; +import org.flowable.task.api.Task; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 流程操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowOnlineOperationService { + + /** + * 保存在线表单的数据,同时启动流程。如果当前用户是第一个用户任务的Assignee, + * 或者第一个用户任务的Assignee是流程发起人变量,该方法还会自动Take第一个任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param table 表对象。 + * @param data 表数据。 + */ + void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data); + + /** + * 保存在线表单的数据,同时启动流程。如果当前用户是第一个用户任务的Assignee, + * 或者第一个用户任务的Assignee是流程发起人变量,该方法还会自动Take第一个任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param slaveDataListMap 关联从表数据Map。 + */ + void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap); + + /** + * 保存在线表单的草稿数据,同时启动一个流程实例。 + * + * @param processDefinitionId 流程定义Id。 + * @param tableId 在线表单主表Id。 + * @param masterData 主表数据。 + * @param slaveData 所有关联从表数据。 + * @return 流程工单对象。 + */ + FlowWorkOrder saveNewDraftAndStartProcess( + String processDefinitionId, Long tableId, JSONObject masterData, JSONObject slaveData); + + /** + * 保存在线表单的数据,同时Take用户任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param table 表对象。 + * @param data 表数据。 + */ + void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data); + + /** + * 保存在线表单的数据,同时Take用户任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param slaveDataListMap 关联从表数据Map。 + */ + void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap); + + /** + * 保存业务表数据,同时接收流程任务。 + * + * @param task 流程任务。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param datasource 主表所在数据源。 + * @param masterData 主表数据。 + * @param masterDataId 主表数据主键。 + * @param slaveDataListMap 从表数据。 + */ + void updateAndTakeTask( + Task task, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineDatasource datasource, + JSONObject masterData, + String masterDataId, + Map> slaveDataListMap); + + /** + * 获取在线表单工作流Id所关联的权限数据,包括权限字列表和权限资源列表。 + * + * @param onlineFormEntryIds 在线表单工作流Id集合。 + * @return 参数中在线表单工作流Id集合所关联的权限数据。 + */ + List> calculatePermData(Set onlineFormEntryIds); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java new file mode 100644 index 00000000..c94dfbf2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.flow.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.flow.base.service.BaseFlowOnlineService; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.online.exception.OnlineRuntimeException; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.online.service.OnlineTableService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.PostConstruct; +import java.util.List; + +/** + * 在线表单和流程监听器进行数据对接时的服务实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSource(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowOnlineBusinessService") +public class FlowOnlineBusinessServiceImpl implements BaseFlowOnlineService { + + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineOperationService onlineOperationService; + + @PostConstruct + public void doRegister() { + flowCustomExtFactory.getOnlineBusinessDataExtHelper().setOnlineBusinessService(this); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFlowStatus(FlowWorkOrder workOrder) { + OnlineTable onlineTable = onlineTableService.getOnlineTableFromCache(workOrder.getOnlineTableId()); + if (onlineTable == null) { + log.error("OnlineTableId [{}] doesn't exist while calling FlowOnlineBusinessServiceImpl.updateFlowStatus", + workOrder.getOnlineTableId()); + return; + } + String dataId = workOrder.getBusinessKey(); + for (OnlineColumn column : onlineTable.getColumnMap().values()) { + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.FLOW_FINISHED_STATUS)) { + onlineOperationService.updateColumn(onlineTable, dataId, column, workOrder.getFlowStatus()); + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.FLOW_APPROVAL_STATUS)) { + onlineOperationService.updateColumn(onlineTable, dataId, column, workOrder.getLatestApprovalStatus()); + } + } + } + + @Override + public void deleteBusinessData(FlowWorkOrder workOrder) { + OnlineTable onlineTable = onlineTableService.getOnlineTableFromCache(workOrder.getOnlineTableId()); + if (onlineTable == null) { + log.error("OnlineTableId [{}] doesn't exist while calling FlowOnlineBusinessServiceImpl.deleteBusinessData", + workOrder.getOnlineTableId()); + return; + } + OnlineDatasource datasource = + onlineDatasourceService.getOnlineDatasourceByMasterTableId(onlineTable.getTableId()); + List relationList = + onlineDatasourceRelationService.getOnlineDatasourceRelationListFromCache(CollUtil.newHashSet(datasource.getDatasourceId())); + String dataId = workOrder.getBusinessKey(); + for (OnlineDatasourceRelation relation : relationList) { + OnlineTable slaveTable = onlineTableService.getOnlineTableFromCache(relation.getSlaveTableId()); + if (slaveTable == null) { + throw new OnlineRuntimeException("数据验证失败,数据源关联 [" + relation.getRelationName() + "] 的从表Id不存在!"); + } + relation.setSlaveTable(slaveTable); + } + onlineOperationService.delete(onlineTable, relationList, dataId); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java new file mode 100644 index 00000000..f7ae1011 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java @@ -0,0 +1,287 @@ +package com.orangeforms.common.flow.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.annotation.MultiDatabaseWriteMethod; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.flow.config.FlowProperties; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.model.FlowEntry; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.online.service.FlowOnlineOperationService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSource(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowOnlineOperationService") +public class FlowOnlineOperationServiceImpl implements FlowOnlineOperationService { + + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private FlowProperties flowProperties; + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data) { + this.saveNewAndStartProcess(processDefinitionId, flowTaskComment, taskVariableData, table, data, null); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap) { + Object dataId = onlineOperationService.saveNewWithRelation(masterTable, masterData, slaveDataListMap); + Assert.notNull(dataId); + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.MASTER_DATA_KEY, masterData); + taskVariableData.put(FlowConstant.SLAVE_DATA_KEY, this.normailizeSlaveDataListMap(slaveDataListMap)); + taskVariableData.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + ProcessInstance instance = flowApiService.start(processDefinitionId, dataId); + flowWorkOrderService.saveNew(instance, dataId, masterTable.getTableId(), null); + flowApiService.takeFirstTask(instance.getProcessInstanceId(), flowTaskComment, taskVariableData); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowWorkOrder saveNewDraftAndStartProcess( + String processDefinitionId, Long tableId, JSONObject masterData, JSONObject slaveData) { + ProcessInstance instance = flowApiService.start(processDefinitionId, null); + return flowWorkOrderService.saveNewWithDraft( + instance, tableId, null, JSON.toJSONString(masterData), JSON.toJSONString(slaveData)); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data) { + this.saveNewAndTakeTask( + processInstanceId, taskId, flowTaskComment, taskVariableData, table, data, null); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap) { + Object dataId = onlineOperationService.saveNewWithRelation(masterTable, masterData, slaveDataListMap); + Assert.notNull(dataId); + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + flowApiService.setBusinessKeyForProcessInstance(processInstanceId, dataId); + Map variables = + flowApiService.initAndGetProcessInstanceVariables(task.getProcessDefinitionId()); + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.putAll(variables); + taskVariableData.put(FlowConstant.MASTER_DATA_KEY, masterData); + taskVariableData.put(FlowConstant.SLAVE_DATA_KEY, this.normailizeSlaveDataListMap(slaveDataListMap)); + taskVariableData.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + flowApiService.completeTask(task, flowTaskComment, taskVariableData); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + FlowWorkOrder flowWorkOrder = + flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(instance.getProcessInstanceId()); + if (flowWorkOrder == null) { + flowWorkOrderService.saveNew(instance, dataId, masterTable.getTableId(), null); + } else { + flowWorkOrder.setBusinessKey(dataId.toString()); + flowWorkOrder.setUpdateTime(new Date()); + flowWorkOrder.setFlowStatus(FlowTaskStatus.SUBMITTED); + flowWorkOrderService.updateById(flowWorkOrder); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void updateAndTakeTask( + Task task, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineDatasource datasource, + JSONObject masterData, + String masterDataId, + Map> slaveDataListMap) { + int flowStatus = FlowTaskStatus.APPROVING; + if (flowTaskComment.getApprovalType().equals(FlowApprovalType.REFUSE)) { + flowStatus = FlowTaskStatus.REFUSED; + } else if (flowTaskComment.getApprovalType().equals(FlowApprovalType.STOP)) { + flowStatus = FlowTaskStatus.FINISHED; + } + OnlineTable masterTable = datasource.getMasterTable(); + Long datasourceId = datasource.getDatasourceId(); + flowWorkOrderService.updateFlowStatusByProcessInstanceId(task.getProcessInstanceId(), flowStatus); + this.updateMasterData(masterTable, masterData, masterDataId); + if (slaveDataListMap != null) { + for (Map.Entry> relationEntry : slaveDataListMap.entrySet()) { + Long relationId = relationEntry.getKey().getRelationId(); + onlineOperationService.updateRelationData( + masterTable, masterData, masterDataId, datasourceId, relationId, relationEntry.getValue()); + } + } + if (flowTaskComment.getApprovalType().equals(FlowApprovalType.STOP)) { + Integer s = MapUtil.getInt(taskVariableData, FlowConstant.LATEST_APPROVAL_STATUS_KEY); + flowWorkOrderService.updateLatestApprovalStatusByProcessInstanceId(task.getProcessInstanceId(), s); + CallResult stopResult = flowApiService.stopProcessInstance( + task.getProcessInstanceId(), flowTaskComment.getTaskComment(), flowStatus); + if (!stopResult.isSuccess()) { + throw new FlowOperationException(stopResult.getErrorMessage()); + } + } else { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.MASTER_DATA_KEY, masterData); + taskVariableData.put(FlowConstant.SLAVE_DATA_KEY, this.normailizeSlaveDataListMap(slaveDataListMap)); + taskVariableData.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + flowApiService.completeTask(task, flowTaskComment, taskVariableData); + } + } + + @Override + public List> calculatePermData(Set onlineFormEntryIds) { + if (CollUtil.isEmpty(onlineFormEntryIds)) { + return new LinkedList<>(); + } + List> permDataList = new LinkedList<>(); + List flowEntries = flowEntryService.getInList(onlineFormEntryIds); + Set pageIds = flowEntries.stream().map(FlowEntry::getPageId).collect(Collectors.toSet()); + Map pageAndVariableNameMap = + onlineDatasourceService.getPageIdAndVariableNameMapByPageIds(pageIds); + for (FlowEntry flowEntry : flowEntries) { + JSONObject permData = new JSONObject(); + permData.put("entryId", flowEntry.getEntryId()); + String key = StrUtil.upperFirst(flowEntry.getProcessDefinitionKey()); + List permCodeList = new LinkedList<>(); + String formPermCode = "form" + key; + permCodeList.add(formPermCode); + permCodeList.add(formPermCode + ":fragment" + key); + permData.put("permCodeList", permCodeList); + String flowUrlPrefix = flowProperties.getUrlPrefix(); + String onlineUrlPrefix = onlineProperties.getUrlPrefix(); + List permList = CollUtil.newLinkedList( + onlineUrlPrefix + "/onlineForm/view", + onlineUrlPrefix + "/onlineForm/render", + onlineUrlPrefix + "/onlineOperation/listByOneToManyRelationId/" + pageAndVariableNameMap.get(flowEntry.getPageId()), + onlineUrlPrefix + "/onlineOperation/uploadByOneToManyRelationId/" + pageAndVariableNameMap.get(flowEntry.getPageId()), + onlineUrlPrefix + "/onlineOperation/dowloadByOneToManyRelationId/" + pageAndVariableNameMap.get(flowEntry.getPageId()), + flowUrlPrefix + "/flowOperation/viewInitialHistoricTaskInfo", + flowUrlPrefix + "/flowOperation/startOnly", + flowUrlPrefix + "/flowOperation/viewInitialTaskInfo", + flowUrlPrefix + "/flowOperation/viewRuntimeTaskInfo", + flowUrlPrefix + "/flowOperation/viewProcessBpmn", + flowUrlPrefix + "/flowOperation/viewHighlightFlowData", + flowUrlPrefix + "/flowOperation/listFlowTaskComment", + flowUrlPrefix + "/flowOperation/cancelWorkOrder", + flowUrlPrefix + "/flowOperation/listRuntimeTask", + flowUrlPrefix + "/flowOperation/listHistoricProcessInstance", + flowUrlPrefix + "/flowOperation/listHistoricTask", + flowUrlPrefix + "/flowOperation/freeJumpTo", + flowUrlPrefix + "/flowOnlineOperation/startPreview", + flowUrlPrefix + "/flowOnlineOperation/viewUserTask", + flowUrlPrefix + "/flowOnlineOperation/viewHistoricProcessInstance", + flowUrlPrefix + "/flowOnlineOperation/submitUserTask", + flowUrlPrefix + "/flowOnlineOperation/upload", + flowUrlPrefix + "/flowOnlineOperation/download", + flowUrlPrefix + "/flowOperation/submitConsign", + flowUrlPrefix + "/flowOnlineOperation/startAndTakeUserTask/" + flowEntry.getProcessDefinitionKey(), + flowUrlPrefix + "/flowOnlineOperation/startAndSaveDraft/" + flowEntry.getProcessDefinitionKey(), + flowUrlPrefix + "/flowOnlineOperation/listWorkOrder/" + flowEntry.getProcessDefinitionKey(), + flowUrlPrefix + "/flowOnlineOperation/printWorkOrder/" + flowEntry.getProcessDefinitionKey() + ); + permData.put("permList", permList); + permDataList.add(permData); + } + return permDataList; + } + + private void updateMasterData(OnlineTable masterTable, JSONObject masterData, String dataId) { + if (masterData == null) { + return; + } + // 如果存在主表数据,就执行主表数据的更新。 + Map originalMasterData = + onlineOperationService.getMasterData(masterTable, null, null, dataId); + for (Map.Entry entry : originalMasterData.entrySet()) { + masterData.putIfAbsent(entry.getKey(), entry.getValue()); + } + if (!onlineOperationService.update(masterTable, masterData)) { + throw new FlowOperationException("主表数据不存在!"); + } + } + + private Map> normailizeSlaveDataListMap( + Map> slaveDataListMap) { + if (slaveDataListMap == null || slaveDataListMap.isEmpty()) { + return null; + } + Map> resultMap = new HashMap<>(slaveDataListMap.size()); + for (Map.Entry> entry : slaveDataListMap.entrySet()) { + resultMap.put(entry.getKey().getSlaveTable().getTableName(), entry.getValue()); + } + return resultMap; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..8ec96e36 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.flow.online.config.FlowOnlineAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/pom.xml new file mode 100644 index 00000000..daad1d91 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/pom.xml @@ -0,0 +1,49 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-flow + 1.0.0 + common-flow + jar + + + + com.orangeforms + common-satoken + 1.0.0 + + + com.orangeforms + common-datafilter + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-log + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + org.flowable + flowable-spring-boot-starter-process + ${flowable.version} + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java new file mode 100644 index 00000000..b7ee7293 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.flow.advice; + +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.flow.exception.FlowEmptyUserException; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.service.FlowTaskCommentService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.common.engine.api.FlowableException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 流程业务层的异常处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Order(1) +@RestControllerAdvice("com.orangeforms") +public class FlowExceptionHandler { + + @Autowired + private FlowTaskCommentService flowTaskCommentService; + + @ExceptionHandler(value = FlowableException.class) + public ResponseResult exceptionHandle(FlowableException ex, HttpServletRequest request) { + if (ex instanceof FlowEmptyUserException) { + FlowEmptyUserException flowEmptyUserException = (FlowEmptyUserException) ex; + FlowTaskComment comment = JSON.parseObject(flowEmptyUserException.getMessage(), FlowTaskComment.class); + flowTaskCommentService.saveNew(comment); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "下一个任务节点的审批人为空,提交被自动驳回!"); + } + log.error("Unhandled FlowException from URL [" + request.getRequestURI() + "]", ex); + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION, ex.getMessage()); + } + + @SuppressWarnings("unchecked") + private T findCause(Throwable ex, Class clazz) { + if (ex.getCause() == null) { + return null; + } + if (ex.getCause().getClass().equals(clazz)) { + return (T) ex.getCause(); + } else { + return this.findCause(ex.getCause(), clazz); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java new file mode 100644 index 00000000..c22362f9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.base.service; + +import com.orangeforms.common.flow.model.FlowWorkOrder; + +/** + * 工作流在线表单的服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseFlowOnlineService { + + /** + * 更新在线表单主表数据的流程状态字段值。 + * + * @param workOrder 工单对象。 + */ + void updateFlowStatus(FlowWorkOrder workOrder); + + /** + * 根据工单对象级联删除业务数据。 + * + * @param workOrder 工单对象。 + */ + void deleteBusinessData(FlowWorkOrder workOrder); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java new file mode 100644 index 00000000..bf9709a6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.flow.config; + +import com.orangeforms.common.core.config.DynamicDataSource; +import com.orangeforms.common.core.constant.ApplicationConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; +import org.flowable.common.engine.impl.EngineConfigurator; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * 服务启动过程中动态切换flowable引擎内置表所在的数据源。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class CustomEngineConfigurator implements EngineConfigurator { + + @Override + public void beforeInit(AbstractEngineConfiguration engineConfiguration) { + DataSource dataSource = engineConfiguration.getDataSource(); + if (dataSource instanceof TransactionAwareDataSourceProxy) { + TransactionAwareDataSourceProxy proxy = (TransactionAwareDataSourceProxy) dataSource; + DataSource targetDataSource = proxy.getTargetDataSource(); + if (targetDataSource instanceof DynamicDataSource) { + DynamicDataSource dynamicDataSource = (DynamicDataSource) targetDataSource; + Map dynamicDataSourceMap = dynamicDataSource.getResolvedDataSources(); + DataSource flowDataSource = dynamicDataSourceMap.get(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE); + if (flowDataSource != null) { + engineConfiguration.setDataSource(flowDataSource); + } + } + } + } + + @Override + public void configure(AbstractEngineConfiguration engineConfiguration) { + // 默认实现。 + } + + @Override + public int getPriority() { + return 0; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java new file mode 100644 index 00000000..a6c7345a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-flow模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({FlowProperties.class}) +public class FlowAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java new file mode 100644 index 00000000..3acf5347 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.flow.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 工作流的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-flow") +public class FlowProperties { + + /** + * 工作落工单操作接口的URL前缀。 + */ + private String urlPrefix; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java new file mode 100644 index 00000000..aa4de82c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流任务触发BUTTON。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowApprovalType { + + /** + * 保存。 + */ + public static final String SAVE = "save"; + /** + * 同意。 + */ + public static final String AGREE = "agree"; + /** + * 拒绝。 + */ + public static final String REFUSE = "refuse"; + /** + * 驳回。 + */ + public static final String REJECT = "reject"; + /** + * 撤销。 + */ + public static final String REVOKE = "revoke"; + /** + * 指派。 + */ + public static final String TRANSFER = "transfer"; + /** + * 多实例会签。 + */ + public static final String MULTI_SIGN = "multi_sign"; + /** + * 会签同意。 + */ + public static final String MULTI_AGREE = "multi_agree"; + /** + * 会签拒绝。 + */ + public static final String MULTI_REFUSE = "multi_refuse"; + /** + * 会签弃权。 + */ + public static final String MULTI_ABSTAIN = "multi_abstain"; + /** + * 多实例加签。 + */ + public static final String MULTI_CONSIGN = "multi_consign"; + /** + * 多实例减签。 + */ + public static final String MULTI_MINUS_SIGN = "multi_minus_sign"; + /** + * 中止。 + */ + public static final String STOP = "stop"; + /** + * 干预。 + */ + public static final String INTERVENE = "intervene"; + /** + * 自由跳转。 + */ + public static final String FREE_JUMP = "free_jump"; + /** + * 流程复活。 + */ + public static final String REUSED = "reused"; + /** + * 流程复活。 + */ + public static final String REVIVE = "revive"; + /** + * 超时自动审批。 + */ + public static final String TIMEOUT_AUTO_COMPLETE = "timeout_auto_complete"; + /** + * 空审批人自动审批。 + */ + public static final String EMPTY_USER_AUTO_COMPLETE = "empty_user_auto_complete"; + /** + * 空审批人自动退回。 + */ + public static final String EMPTY_USER_AUTO_REJECT = "empty_user_auto_reject"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowApprovalType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java new file mode 100644 index 00000000..495831b8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.flow.constant; + +/** + * 待办任务回退类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowBackType { + + /** + * 驳回。 + */ + public static final int REJECT = 0; + /** + * 撤回。 + */ + public static final int REVOKE = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowBackType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java new file mode 100644 index 00000000..cdb89485 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.flow.constant; + +/** + * 内置的流程审批状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowBuiltinApprovalStatus { + + /** + * 同意。 + */ + public static final int AGREED = 1; + /** + * 拒绝。 + */ + public static final int REFUSED = 2; + /** + * 会签同意。 + */ + public static final int MULTI_AGREED = 3; + /** + * 会签拒绝。 + */ + public static final int MULTI_REFUSED = 4; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowBuiltinApprovalStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java new file mode 100644 index 00000000..12ccf122 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java @@ -0,0 +1,266 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流中的常量数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowConstant { + + /** + * 标识流程实例启动用户的变量名。 + */ + public static final String START_USER_NAME_VAR = "${startUserName}"; + + /** + * 流程实例发起人变量名。 + */ + public static final String PROC_INSTANCE_INITIATOR_VAR = "initiator"; + + /** + * 流程实例中发起人用户的变量名。 + */ + public static final String PROC_INSTANCE_START_USER_NAME_VAR = "startUserName"; + + /** + * 流程任务的指定人变量。 + */ + public static final String TASK_APPOINTED_ASSIGNEE_VAR = "appointedAssignee"; + + /** + * 操作类型变量。 + */ + public static final String OPERATION_TYPE_VAR = "operationType"; + + /** + * 提交用户。 + */ + public static final String SUBMIT_USER_VAR = "submitUser"; + + /** + * 多任务拒绝数量变量。 + */ + public static final String MULTI_REFUSE_COUNT_VAR = "multiRefuseCount"; + + /** + * 多任务同意数量变量。 + */ + public static final String MULTI_AGREE_COUNT_VAR = "multiAgreeCount"; + + /** + * 多任务弃权数量变量。 + */ + public static final String MULTI_ABSTAIN_COUNT_VAR = "multiAbstainCount"; + + /** + * 会签发起任务。 + */ + public static final String MULTI_SIGN_START_TASK_VAR = "multiSignStartTask"; + + /** + * 会签任务总数量。 + */ + public static final String MULTI_SIGN_NUM_OF_INSTANCES_VAR = "multiNumOfInstances"; + + /** + * 会签任务执行的批次Id。 + */ + public static final String MULTI_SIGN_TASK_EXECUTION_ID_VAR = "taskExecutionId"; + + /** + * 多实例实例数量变量。 + */ + public static final String NUMBER_OF_INSTANCES_VAR = "nrOfInstances"; + + /** + * 多实例已完成实例数量变量。 + */ + public static final String NUMBER_OF_COMPLETED_INSTANCES_VAR = "nrOfCompletedInstances"; + + /** + * 多任务指派人列表变量。 + */ + public static final String MULTI_ASSIGNEE_LIST_VAR = "assigneeList"; + + /** + * 上级部门领导审批变量。 + */ + public static final String GROUP_TYPE_UP_DEPT_POST_LEADER_VAR = "upDeptPostLeader"; + + /** + * 本部门领导审批变量。 + */ + public static final String GROUP_TYPE_DEPT_POST_LEADER_VAR = "deptPostLeader"; + + /** + * 所有部门岗位审批变量。 + */ + public static final String GROUP_TYPE_ALL_DEPT_POST_VAR = "allDeptPost"; + + /** + * 本部门岗位审批变量。 + */ + public static final String GROUP_TYPE_SELF_DEPT_POST_VAR = "selfDeptPost"; + + /** + * 同级部门岗位审批变量。 + */ + public static final String GROUP_TYPE_SIBLING_DEPT_POST_VAR = "siblingDeptPost"; + + /** + * 上级部门岗位审批变量。 + */ + public static final String GROUP_TYPE_UP_DEPT_POST_VAR = "upDeptPost"; + + /** + * 任意部门关联的岗位审批变量。 + */ + public static final String GROUP_TYPE_DEPT_POST_VAR = "deptPost"; + + /** + * 指定角色分组变量。 + */ + public static final String GROUP_TYPE_ROLE_VAR = "role"; + + /** + * 指定部门分组变量。 + */ + public static final String GROUP_TYPE_DEPT_VAR = "dept"; + + /** + * 指定用户分组变量。 + */ + public static final String GROUP_TYPE_USER_VAR = "user"; + + /** + * 指定审批人。 + */ + public static final String GROUP_TYPE_ASSIGNEE = "ASSIGNEE"; + + /** + * 岗位。 + */ + public static final String GROUP_TYPE_POST = "POST"; + + /** + * 上级部门领导审批。 + */ + public static final String GROUP_TYPE_UP_DEPT_POST_LEADER = "UP_DEPT_POST_LEADER"; + + /** + * 本部门岗位领导审批。 + */ + public static final String GROUP_TYPE_DEPT_POST_LEADER = "DEPT_POST_LEADER"; + + /** + * 本部门岗位前缀。 + */ + public static final String SELF_DEPT_POST_PREFIX = "SELF_DEPT_"; + + /** + * 上级部门岗位前缀。 + */ + public static final String UP_DEPT_POST_PREFIX = "UP_DEPT_"; + + /** + * 同级部门岗位前缀。 + */ + public static final String SIBLING_DEPT_POST_PREFIX = "SIBLING_DEPT_"; + + /** + * 当前流程实例所有任务的抄送数据前缀。 + */ + public static final String COPY_DATA_MAP_PREFIX = "copyDataMap_"; + + /** + * 作为临时变量存入任务变量JSONObject对象时的key。 + */ + public static final String COPY_DATA_KEY = "copyDataKey"; + + /** + * 流程中业务快照数据中,主表数据的Key。 + */ + public static final String MASTER_DATA_KEY = "masterData"; + + /** + * 流程中业务快照数据中,关联从表数据的Key。 + */ + public static final String SLAVE_DATA_KEY = "slaveData"; + + /** + * 流程任务的最近更新状态的Key。 + */ + public static final String LATEST_APPROVAL_STATUS_KEY = "latestApprovalStatus"; + + /** + * 流程用户任务待办之前的通知类型的Key。 + */ + public static final String USER_TASK_NOTIFY_TYPES_KEY = "flowNotifyTypeList"; + + /** + * 流程用户任务自动跳过类型的Key。 + */ + public static final String USER_TASK_AUTO_SKIP_KEY = "autoSkipType"; + + /** + * 流程用户任务驳回类型的Key。 + */ + public static final String USER_TASK_REJECT_TYPE_KEY = "rejectType"; + + /** + * 驳回时携带的变量数据。 + */ + public static final String REJECT_TO_SOURCE_DATA_VAR = "rejectData"; + + /** + * 驳回时携带的变量数据。 + */ + public static final String REJECT_BACK_TO_SOURCE_DATA_VAR = "rejectBackData"; + + /** + * 指定审批人。 + */ + public static final String DELEGATE_ASSIGNEE_VAR = "defaultAssignee"; + + /** + * 业务主表对象的键。目前仅仅用户在线表单工作流。 + */ + public static final String MASTER_TABLE_KEY = "masterTable"; + + /** + * 不删除任务超时作业。 + */ + public static final String NOT_DELETE_TIMEOUT_TASK_JOB_KEY = "notDeleteTimeoutTaskJob"; + + /** + * 用户任务超时小时数。 + */ + public static final String TASK_TIMEOUT_HOURS = "timeoutHours"; + + /** + * 用户任务超时处理方式。 + */ + public static final String TASK_TIMEOUT_HANDLE_WAY = "timeoutHandleWay"; + + /** + * 用户任务超时指定审批人。 + */ + public static final String TASK_TIMEOUT_DEFAULT_ASSIGNEE = "defaultAssignee"; + + /** + * 空处理人处理方式。 + */ + public static final String EMPTY_USER_HANDLE_WAY = "emptyUserHandleWay"; + + /** + * 空处理人时指定的审批人。 + */ + public static final String EMPTY_USER_TO_ASSIGNEE = "emptyUserToAssignee"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowConstant() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java new file mode 100644 index 00000000..d25ec6e4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流任务类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowTaskStatus { + + /** + * 已提交。 + */ + public static final int SUBMITTED = 0; + /** + * 审批中。 + */ + public static final int APPROVING = 1; + /** + * 被拒绝。 + */ + public static final int REFUSED = 2; + /** + * 已结束。 + */ + public static final int FINISHED = 3; + /** + * 提前停止。 + */ + public static final Integer STOPPED = 4; + /** + * 已取消。 + */ + public static final Integer CANCELLED = 5; + /** + * 保存草稿。 + */ + public static final Integer DRAFT = 6; + /** + * 流程复活。 + */ + public static final Integer REVIVE = 7; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowTaskStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java new file mode 100644 index 00000000..8d97ba9b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流任务类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowTaskType { + + /** + * 其他类型任务。 + */ + public static final int OTHER_TYPE = 0; + /** + * 用户任务。 + */ + public static final int USER_TYPE = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowTaskType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java new file mode 100644 index 00000000..95558d08 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java @@ -0,0 +1,232 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.flow.dto.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.vo.*; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * 工作流流程分类接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程分类接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowCategory") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowCategoryController { + + @Autowired + private FlowCategoryService flowCategoryService; + @Autowired + private FlowEntryService flowEntryService; + + /** + * 新增FlowCategory数据。 + * + * @param flowCategoryDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"flowCategoryDto.categoryId"}) + @SaCheckPermission("flowCategory.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody FlowCategoryDto flowCategoryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowCategoryDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowCategory flowCategory = MyModelUtil.copyTo(flowCategoryDto, FlowCategory.class); + if (flowCategoryService.existByCode(flowCategory.getCode())) { + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, "数据验证失败,当前流程分类已经存在!"); + } + flowCategory = flowCategoryService.saveNew(flowCategory); + return ResponseResult.success(flowCategory.getCategoryId()); + } + + /** + * 更新FlowCategory数据。 + * + * @param flowCategoryDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowCategory.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody FlowCategoryDto flowCategoryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowCategoryDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowCategory flowCategory = MyModelUtil.copyTo(flowCategoryDto, FlowCategory.class); + ResponseResult verifyResult = this.doVerifyAndGet(flowCategory.getCategoryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowCategory originalFlowCategory = verifyResult.getData(); + if (!StrUtil.equals(flowCategory.getCode(), originalFlowCategory.getCode())) { + FlowEntry filter = new FlowEntry(); + filter.setCategoryId(flowCategory.getCategoryId()); + filter.setStatus(FlowEntryStatus.PUBLISHED); + List flowEntryList = flowEntryService.getListByFilter(filter); + if (CollUtil.isNotEmpty(flowEntryList)) { + errorMessage = "数据验证失败,当前流程分类存在已经发布的流程数据,因此分类标识不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowCategoryService.existByCode(flowCategory.getCode())) { + errorMessage = "数据验证失败,当前流程分类已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + } + if (!flowCategoryService.update(flowCategory, originalFlowCategory)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除FlowCategory数据。 + * + * @param categoryId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowCategory.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long categoryId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(categoryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry filter = new FlowEntry(); + filter.setCategoryId(categoryId); + List flowEntryList = flowEntryService.getListByFilter(filter); + if (CollUtil.isNotEmpty(flowEntryList)) { + errorMessage = "数据验证失败,请先删除当前流程分类关联的流程数据!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowCategoryService.remove(categoryId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的FlowCategory列表。 + * + * @param flowCategoryDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("flowCategory.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody FlowCategoryDto flowCategoryDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + FlowCategory flowCategoryFilter = MyModelUtil.copyTo(flowCategoryDtoFilter, FlowCategory.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowCategory.class); + List flowCategoryList = flowCategoryService.getFlowCategoryListWithRelation(flowCategoryFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(flowCategoryList, FlowCategoryVo.class)); + } + + /** + * 查看指定FlowCategory对象详情。 + * + * @param categoryId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("flowCategory.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long categoryId) { + ResponseResult verifyResult = this.doVerifyAndGet(categoryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + return ResponseResult.success(verifyResult.getData(), FlowCategoryVo.class); + } + + /** + * 以字典形式返回全部FlowCategory数据集合。字典的键值为[categoryId, name]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject FlowCategoryDto filter) { + List resultList = + flowCategoryService.getFlowCategoryList(MyModelUtil.copyTo(filter, FlowCategory.class), null); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, FlowCategory::getCategoryId, FlowCategory::getName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = flowCategoryService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, FlowCategory::getCategoryId, FlowCategory::getName)); + } + + private ResponseResult doVerifyAndGet(Long categoryId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(categoryId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + FlowCategory flowCategory = flowCategoryService.getById(categoryId); + if (flowCategory == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(flowCategory.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用并不存在该流程分类的定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(flowCategory.getTenantId(), tokenData.getTenantId())) { + errorMessage = "数据验证失败,当前租户并不存在该流程分类的定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(flowCategory); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java new file mode 100644 index 00000000..855e59de --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java @@ -0,0 +1,475 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.flow.constant.FlowTaskType; +import com.orangeforms.common.flow.dto.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.vo.*; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import javax.xml.stream.XMLStreamException; +import java.util.*; + +/** + * 工作流流程定义接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程定义接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowEntry") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowEntryController { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowCategoryService flowCategoryService; + @Autowired + private FlowEntryVariableService flowEntryVariableService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowTaskExtService flowTaskExtService; + + /** + * 新增工作流对象数据。 + * + * @param flowEntryDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"flowEntryDto.entryId"}) + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody FlowEntryDto flowEntryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntry flowEntry = MyModelUtil.copyTo(flowEntryDto, FlowEntry.class); + if (flowEntryService.existByProcessDefinitionKey(flowEntry.getProcessDefinitionKey())) { + errorMessage = "数据验证失败,该流程定义标识已存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = flowEntryService.verifyRelatedData(flowEntry, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntry = flowEntryService.saveNew(flowEntry); + return ResponseResult.success(flowEntry.getEntryId()); + } + + /** + * 更新工作流对象数据。 + * + * @param flowEntryDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody FlowEntryDto flowEntryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntry flowEntry = MyModelUtil.copyTo(flowEntryDto, FlowEntry.class); + ResponseResult verifyResult = this.doVerifyAndGet(flowEntry.getEntryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry originalFlowEntry = verifyResult.getData(); + if (ObjectUtil.notEqual(flowEntry.getProcessDefinitionKey(), originalFlowEntry.getProcessDefinitionKey())) { + if (originalFlowEntry.getStatus().equals(FlowEntryStatus.PUBLISHED)) { + errorMessage = "数据验证失败,当前流程为发布状态,流程标识不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowEntryService.existByProcessDefinitionKey(flowEntry.getProcessDefinitionKey())) { + errorMessage = "数据验证失败,该流程定义标识已存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + // 验证关联Id的数据合法性 + CallResult callResult = flowEntryService.verifyRelatedData(flowEntry, originalFlowEntry); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowEntryService.update(flowEntry, originalFlowEntry)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除工作流对象数据。 + * + * @param entryId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long entryId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(entryId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry originalFlowEntry = verifyResult.getData(); + if (originalFlowEntry.getStatus().equals(FlowEntryStatus.PUBLISHED)) { + errorMessage = "数据验证失败,当前流程为发布状态,不能删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowEntryService.remove(entryId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 发布工作流。 + * + * @param entryId 流程主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.PUBLISH) + @PostMapping("/publish") + public ResponseResult publish(@MyRequestBody(required = true) Long entryId) throws XMLStreamException { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry flowEntry = verifyResult.getData(); + if (StrUtil.isBlank(flowEntry.getBpmnXml())) { + errorMessage = "数据验证失败,该流程没有流程图不能被发布!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult taskInfoResult = this.verifyAndGetInitialTaskInfo(flowEntry); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + String taskInfo = taskInfoResult.getData() == null ? null : JSON.toJSONString(taskInfoResult.getData()); + flowEntryService.publish(flowEntry, taskInfo); + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的工作流列表。 + * + * @param flowEntryDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("flowEntry.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody FlowEntryDto flowEntryDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + FlowEntry flowEntryFilter = MyModelUtil.copyTo(flowEntryDtoFilter, FlowEntry.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowEntry.class); + List flowEntryList = flowEntryService.getFlowEntryListWithRelation(flowEntryFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(flowEntryList, FlowEntryVo.class)); + } + + /** + * 查看指定工作流对象详情。 + * + * @param entryId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("flowEntry.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long entryId) { + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry flowEntry = flowEntryService.getByIdWithRelation(entryId, MyRelationParam.full()); + if (flowEntry == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(flowEntry, FlowEntryVo.class); + } + + /** + * 列出指定流程的发布版本列表。 + * + * @param entryId 流程主键Id。 + * @return 应答结果对象,包含流程发布列表数据。 + */ + @SaCheckPermission("flowEntry.all") + @GetMapping("/listFlowEntryPublish") + public ResponseResult> listFlowEntryPublish(@RequestParam Long entryId) { + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + List flowEntryPublishList = flowEntryService.getFlowEntryPublishList(entryId); + return ResponseResult.success(MyModelUtil.copyCollectionTo(flowEntryPublishList, FlowEntryPublishVo.class)); + } + + /** + * 以字典形式返回全部FlowEntry数据集合。字典的键值为[entryId, procDefinitionName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject FlowEntryDto filter) { + List resultList = + flowEntryService.getFlowEntryList(MyModelUtil.copyTo(filter, FlowEntry.class), null); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, FlowEntry::getEntryId, FlowEntry::getProcessDefinitionName)); + } + + /** + * 获取所有流程分类和流程定义的列表。白名单接口。 + * + * @return 所有流程分类和流程定义的列表 + */ + @GetMapping("/listAll") + public ResponseResult listAll() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("flowEntryList", flowEntryService.getFlowEntryList(null, null)); + jsonObject.put("flowCategoryList", flowCategoryService.getFlowCategoryList(null, null)); + return ResponseResult.success(jsonObject); + } + + /** + * 白名单接口,根据流程Id,获取流程引擎需要的流程标识和流程名称。 + * + * @param entryId 流程Id。 + * @return 流程的部分数据。 + */ + @GetMapping("/viewDict") + public ResponseResult> viewDict(@RequestParam Long entryId) { + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry flowEntry = verifyResult.getData(); + Map resultMap = new HashMap<>(2); + resultMap.put("processDefinitionKey", flowEntry.getProcessDefinitionKey()); + resultMap.put("processDefinitionName", flowEntry.getProcessDefinitionName()); + return ResponseResult.success(resultMap); + } + + /** + * 切换指定工作的发布主版本。 + * + * @param entryId 工作流主键Id。 + * @param newEntryPublishId 新的工作流发布主版本对象的主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateMainVersion") + public ResponseResult updateMainVersion( + @MyRequestBody(required = true) Long entryId, + @MyRequestBody(required = true) Long newEntryPublishId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntryPublish flowEntryPublish = flowEntryService.getFlowEntryPublishFromCache(newEntryPublishId); + if (flowEntryPublish == null) { + errorMessage = "数据验证失败,当前流程发布版本并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (ObjectUtil.notEqual(entryId, flowEntryPublish.getEntryId())) { + errorMessage = "数据验证失败,当前工作流并不包含该工作流发布版本数据,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (BooleanUtil.isTrue(flowEntryPublish.getMainVersion())) { + errorMessage = "数据验证失败,该版本已经为当前工作流的发布主版本,不能重复设置!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntryService.updateFlowEntryMainVersion(flowEntryService.getById(entryId), flowEntryPublish); + return ResponseResult.success(); + } + + /** + * 挂起工作流的指定发布版本。 + * + * @param entryPublishId 工作发布Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.SUSPEND) + @PostMapping("/suspendFlowEntryPublish") + public ResponseResult suspendFlowEntryPublish(@MyRequestBody(required = true) Long entryPublishId) { + String errorMessage; + FlowEntryPublish flowEntryPublish = flowEntryService.getFlowEntryPublishFromCache(entryPublishId); + if (flowEntryPublish == null) { + errorMessage = "数据验证失败,当前流程发布版本并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerifyAndGet(flowEntryPublish.getEntryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (BooleanUtil.isFalse(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布版本已处于挂起状态!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntryService.suspendFlowEntryPublish(flowEntryPublish); + return ResponseResult.success(); + } + + /** + * 激活工作流的指定发布版本。 + * + * @param entryPublishId 工作发布Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.RESUME) + @PostMapping("/activateFlowEntryPublish") + public ResponseResult activateFlowEntryPublish(@MyRequestBody(required = true) Long entryPublishId) { + String errorMessage; + FlowEntryPublish flowEntryPublish = flowEntryService.getFlowEntryPublishFromCache(entryPublishId); + if (flowEntryPublish == null) { + errorMessage = "数据验证失败,当前流程发布版本并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerifyAndGet(flowEntryPublish.getEntryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (BooleanUtil.isTrue(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布版本已处于激活状态!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntryService.activateFlowEntryPublish(flowEntryPublish); + return ResponseResult.success(); + } + + private ResponseResult doVerifyAndGet(Long entryId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(entryId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + FlowEntry flowEntry = flowEntryService.getById(entryId); + if (flowEntry == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(flowEntry.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用并不存在该流程定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(flowEntry.getTenantId(), tokenData.getTenantId())) { + errorMessage = "数据验证失败,当前租户并不存在该流程定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(flowEntry); + } + + private ResponseResult verifyAndGetInitialTaskInfo(FlowEntry flowEntry) throws XMLStreamException { + String errorMessage; + BpmnModel bpmnModel = flowApiService.convertToBpmnModel(flowEntry.getBpmnXml()); + Process process = bpmnModel.getMainProcess(); + if (process == null) { + errorMessage = "数据验证失败,当前流程标识 [" + flowEntry.getProcessDefinitionKey() + "] 关联的流程模型并不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Collection elementList = process.getFlowElements(); + FlowElement startEvent = null; + // 这里我们只定位流程模型中的第二个节点。 + for (FlowElement flowElement : elementList) { + if (flowElement instanceof StartEvent) { + startEvent = flowElement; + break; + } + } + if (startEvent == null) { + errorMessage = "数据验证失败,当前流程图没有包含 [开始事件] 节点,请修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowElement firstTask = this.findFirstTask(elementList, startEvent); + if (firstTask == null) { + errorMessage = "数据验证失败,当前流程图没有包含 [开始事件] 节点没有任何连线,请修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfoVo; + if (firstTask instanceof UserTask) { + UserTask userTask = (UserTask) firstTask; + String formKey = userTask.getFormKey(); + if (StrUtil.isNotBlank(formKey)) { + taskInfoVo = JSON.parseObject(formKey, TaskInfoVo.class); + } else { + taskInfoVo = new TaskInfoVo(); + } + taskInfoVo.setAssignee(userTask.getAssignee()); + taskInfoVo.setTaskKey(userTask.getId()); + taskInfoVo.setTaskType(FlowTaskType.USER_TYPE); + Map> extensionMap = userTask.getExtensionElements(); + if (MapUtil.isNotEmpty(extensionMap)) { + taskInfoVo.setOperationList(flowTaskExtService.buildOperationListExtensionElement(extensionMap)); + taskInfoVo.setVariableList(flowTaskExtService.buildVariableListExtensionElement(extensionMap)); + } + } else { + taskInfoVo = new TaskInfoVo(); + taskInfoVo.setTaskType(FlowTaskType.OTHER_TYPE); + } + return ResponseResult.success(taskInfoVo); + } + + private FlowElement findFirstTask(Collection elementList, FlowElement startEvent) { + for (FlowElement flowElement : elementList) { + if (flowElement instanceof SequenceFlow) { + SequenceFlow sequenceFlow = (SequenceFlow) flowElement; + if (sequenceFlow.getSourceFlowElement().equals(startEvent)) { + return sequenceFlow.getTargetFlowElement(); + } + } + } + return null; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java new file mode 100644 index 00000000..371d37cc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java @@ -0,0 +1,159 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.flow.vo.*; +import com.orangeforms.common.flow.dto.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import jakarta.validation.groups.Default; + +/** + * 工作流流程变量接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程变量接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowEntryVariable") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowEntryVariableController { + + @Autowired + private FlowEntryVariableService flowEntryVariableService; + + /** + * 新增流程变量数据。 + * + * @param flowEntryVariableDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"flowEntryVariableDto.variableId"}) + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody FlowEntryVariableDto flowEntryVariableDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryVariableDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntryVariable flowEntryVariable = MyModelUtil.copyTo(flowEntryVariableDto, FlowEntryVariable.class); + flowEntryVariable = flowEntryVariableService.saveNew(flowEntryVariable); + return ResponseResult.success(flowEntryVariable.getVariableId()); + } + + /** + * 更新流程变量数据。 + * + * @param flowEntryVariableDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody FlowEntryVariableDto flowEntryVariableDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryVariableDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntryVariable flowEntryVariable = MyModelUtil.copyTo(flowEntryVariableDto, FlowEntryVariable.class); + FlowEntryVariable originalFlowEntryVariable = flowEntryVariableService.getById(flowEntryVariable.getVariableId()); + if (originalFlowEntryVariable == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!flowEntryVariableService.update(flowEntryVariable, originalFlowEntryVariable)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除流程变量数据。 + * + * @param variableId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long variableId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(variableId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + FlowEntryVariable originalFlowEntryVariable = flowEntryVariableService.getById(variableId); + if (originalFlowEntryVariable == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!flowEntryVariableService.remove(variableId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的流程变量列表。 + * + * @param flowEntryVariableDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("flowEntry.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody FlowEntryVariableDto flowEntryVariableDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + FlowEntryVariable flowEntryVariableFilter = MyModelUtil.copyTo(flowEntryVariableDtoFilter, FlowEntryVariable.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowEntryVariable.class); + List flowEntryVariableList = + flowEntryVariableService.getFlowEntryVariableListWithRelation(flowEntryVariableFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(flowEntryVariableList, FlowEntryVariableVo.class)); + } + + /** + * 查看指定流程变量对象详情。 + * + * @param variableId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("flowEntry.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long variableId) { + if (MyCommonUtil.existBlankArgument(variableId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + FlowEntryVariable flowEntryVariable = flowEntryVariableService.getByIdWithRelation(variableId, MyRelationParam.full()); + if (flowEntryVariable == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(flowEntryVariable, FlowEntryVariableVo.class); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java new file mode 100644 index 00000000..ffcc00b6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java @@ -0,0 +1,110 @@ +package com.orangeforms.common.flow.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.flow.model.constant.FlowMessageType; +import com.orangeforms.common.flow.model.FlowMessage; +import com.orangeforms.common.flow.service.FlowMessageService; +import com.orangeforms.common.flow.vo.FlowMessageVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 工作流消息接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流消息接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowMessage") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowMessageController { + + @Autowired + private FlowMessageService flowMessageService; + + /** + * 获取当前用户的未读消息总数。 + * NOTE:白名单接口。 + * + * @return 应答结果对象,包含当前用户的未读消息总数。 + */ + @GetMapping("/getMessageCount") + public ResponseResult getMessageCount() { + JSONObject resultData = new JSONObject(); + resultData.put("remindingMessageCount", flowMessageService.countRemindingMessageListByUser()); + resultData.put("copyMessageCount", flowMessageService.countCopyMessageByUser()); + return ResponseResult.success(resultData); + } + + /** + * 获取当前用户的催办消息列表。 + * 不仅仅包含,其中包括当前用户所属角色、部门和岗位的候选组催办消息。 + * NOTE:白名单接口。 + * + * @return 应答结果对象,包含查询结果集。 + */ + @PostMapping("/listRemindingTask") + public ResponseResult> listRemindingTask(@MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + List flowMessageList = flowMessageService.getRemindingMessageListByUser(); + return ResponseResult.success(MyPageUtil.makeResponseData(flowMessageList, FlowMessageVo.class)); + } + + /** + * 获取当前用户的抄送消息列表。 + * 不仅仅包含,其中包括当前用户所属角色、部门和岗位的候选组抄送消息。 + * NOTE:白名单接口。 + * + * @param read true表示已读,false表示未读。 + * @return 应答结果对象,包含查询结果集。 + */ + @PostMapping("/listCopyMessage") + public ResponseResult> listCopyMessage( + @MyRequestBody MyPageParam pageParam, @MyRequestBody Boolean read) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + List flowMessageList = flowMessageService.getCopyMessageListByUser(read); + return ResponseResult.success(MyPageUtil.makeResponseData(flowMessageList, FlowMessageVo.class)); + } + + /** + * 读取抄送消息,同时更新当前用户对指定抄送消息的读取状态。 + * + * @param messageId 消息Id。 + * @return 应答结果对象。 + */ + @PostMapping("/readCopyTask") + public ResponseResult readCopyTask(@MyRequestBody Long messageId) { + String errorMessage; + // 验证流程任务的合法性。 + FlowMessage flowMessage = flowMessageService.getById(messageId); + if (flowMessage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (flowMessage.getMessageType() != FlowMessageType.COPY_TYPE) { + errorMessage = "数据验证失败,当前消息不是抄送类型消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowMessageService.isCandidateIdentityOnMessage(messageId)) { + errorMessage = "数据验证失败,当前用户没有权限访问该消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowMessageService.readCopyTask(messageId); + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java new file mode 100644 index 00000000..033d7e2c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java @@ -0,0 +1,941 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.util.FlowOperationHelper; +import com.orangeforms.common.flow.vo.FlowTaskCommentVo; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 工作流流程操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程操作接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowOperation") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowOperationController { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowTaskCommentService flowTaskCommentService; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowMessageService flowMessageService; + @Autowired + private FlowOperationHelper flowOperationHelper; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowMultiInstanceTransService flowMultiInstanceTransService; + + private static final String ACTIVE_MULTI_INST_TASK = "activeMultiInstanceTask"; + private static final String SHOW_NAME = "showName"; + private static final String INSTANCE_ID = "processInstanceId"; + + /** + * 获取开始节点之后的第一个任务节点的数据。 + * + * @param processDefinitionKey 流程标识。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewInitialTaskInfo") + public ResponseResult viewInitialTaskInfo(@RequestParam String processDefinitionKey) { + ResponseResult flowEntryResult = flowOperationHelper.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + String initTaskInfo = flowEntryPublish.getInitTaskInfo(); + TaskInfoVo taskInfo = StrUtil.isBlank(initTaskInfo) + ? null : JSON.parseObject(initTaskInfo, TaskInfoVo.class); + if (taskInfo != null) { + String loginName = TokenData.takeFromRequest().getLoginName(); + taskInfo.setAssignedMe(StrUtil.equalsAny( + taskInfo.getAssignee(), loginName, FlowConstant.START_USER_NAME_VAR)); + } + return ResponseResult.success(taskInfo); + } + + /** + * 获取流程运行时指定任务的信息。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @param processInstanceId 流程引擎的实例Id。 + * @param taskId 流程引擎的任务Id。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewRuntimeTaskInfo") + public ResponseResult viewRuntimeTaskInfo( + @RequestParam String processDefinitionId, + @RequestParam String processInstanceId, + @RequestParam String taskId) { + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfoVo = taskInfoResult.getData(); + FlowTaskExt flowTaskExt = + flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, taskInfoVo.getTaskKey()); + if (flowTaskExt != null) { + if (StrUtil.isNotBlank(flowTaskExt.getOperationListJson())) { + taskInfoVo.setOperationList(JSON.parseArray(flowTaskExt.getOperationListJson(), JSONObject.class)); + } + if (StrUtil.isNotBlank(flowTaskExt.getVariableListJson())) { + taskInfoVo.setVariableList(JSON.parseArray(flowTaskExt.getVariableListJson(), JSONObject.class)); + } + } + return ResponseResult.success(taskInfoVo); + } + + /** + * 获取流程运行时指定任务的信息。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @param processInstanceId 流程引擎的实例Id。 + * @param taskId 流程引擎的任务Id。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewHistoricTaskInfo") + public ResponseResult viewHistoricTaskInfo( + @RequestParam String processDefinitionId, + @RequestParam String processInstanceId, + @RequestParam String taskId) { + String errorMessage; + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + String loginName = TokenData.takeFromRequest().getLoginName(); + if (!StrUtil.equals(taskInstance.getAssignee(), loginName)) { + errorMessage = "数据验证失败,当前用户不是指派人!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfoVo = JSON.parseObject(taskInstance.getFormKey(), TaskInfoVo.class); + FlowTaskExt flowTaskExt = + flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, taskInstance.getTaskDefinitionKey()); + if (flowTaskExt != null) { + if (StrUtil.isNotBlank(flowTaskExt.getOperationListJson())) { + taskInfoVo.setOperationList(JSON.parseArray(flowTaskExt.getOperationListJson(), JSONObject.class)); + } + if (StrUtil.isNotBlank(flowTaskExt.getVariableListJson())) { + taskInfoVo.setVariableList(JSON.parseArray(flowTaskExt.getVariableListJson(), JSONObject.class)); + } + } + return ResponseResult.success(taskInfoVo); + } + + /** + * 获取第一个提交表单数据的任务信息。 + * + * @param processInstanceId 流程实例Id。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewInitialHistoricTaskInfo") + public ResponseResult viewInitialHistoricTaskInfo(@RequestParam String processInstanceId) { + String errorMessage; + List taskCommentList = + flowTaskCommentService.getFlowTaskCommentList(processInstanceId); + if (CollUtil.isEmpty(taskCommentList)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + FlowTaskComment taskComment = taskCommentList.get(0); + HistoricTaskInstance task = flowApiService.getHistoricTaskInstance(processInstanceId, taskComment.getTaskId()); + if (StrUtil.isBlank(task.getFormKey())) { + errorMessage = "数据验证失败,指定任务的formKey属性不存在,请重新修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfo = JSON.parseObject(task.getFormKey(), TaskInfoVo.class); + taskInfo.setTaskKey(task.getTaskDefinitionKey()); + return ResponseResult.success(taskInfo); + } + + /** + * 获取任务的用户信息列表。 + * + * @param processDefinitionId 流程定义Id。 + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param historic 是否为历史任务。 + * @return 任务相关的用户信息列表。 + */ + @DisableDataFilter + @GetMapping("/viewTaskUserInfo") + public ResponseResult> viewTaskUserInfo( + @RequestParam String processDefinitionId, + @RequestParam String processInstanceId, + @RequestParam String taskId, + @RequestParam Boolean historic) { + TaskInfo taskInfo; + HistoricTaskInstance hisotricTask; + if (BooleanUtil.isFalse(historic)) { + taskInfo = flowApiService.getTaskById(taskId); + if (taskInfo == null) { + hisotricTask = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + taskInfo = hisotricTask; + historic = true; + } + } else { + hisotricTask = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + taskInfo = hisotricTask; + } + if (taskInfo == null) { + String errorMessage = "数据验证失败,任务Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + String taskKey = taskInfo.getTaskDefinitionKey(); + FlowTaskExt taskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, taskKey); + boolean isMultiInstanceTask = flowApiService.isMultiInstanceTask(taskInfo.getProcessDefinitionId(), taskKey); + List resultUserInfoList = + flowTaskExtService.getCandidateUserInfoList(processInstanceId, taskExt, taskInfo, isMultiInstanceTask, historic); + if (BooleanUtil.isTrue(historic) || isMultiInstanceTask) { + List taskCommentList = buildApprovedFlowTaskCommentList(taskInfo, isMultiInstanceTask); + Map resultUserInfoMap = + resultUserInfoList.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + for (FlowTaskComment taskComment : taskCommentList) { + FlowUserInfoVo flowUserInfoVo = resultUserInfoMap.get(taskComment.getCreateLoginName()); + if (flowUserInfoVo != null) { + flowUserInfoVo.setLastApprovalTime(taskComment.getCreateTime()); + } + } + } + return ResponseResult.success(resultUserInfoList); + } + + /** + * 获取多实例会签任务的指派人列表。 + * NOTE: 白名单接口。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 多实例任务的上一级任务Id。 + * @return 应答结果,指定会签任务的指派人列表。 + */ + @GetMapping("/listMultiSignAssignees") + public ResponseResult> listMultiSignAssignees( + @RequestParam String processInstanceId, @RequestParam String taskId) { + ResponseResult verifyResult = this.doVerifyMultiSign(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + Task activeMultiInstanceTask = + verifyResult.getData().getObject(ACTIVE_MULTI_INST_TASK, Task.class); + String multiInstanceExecId = flowApiService.getExecutionVariableStringWithSafe( + activeMultiInstanceTask.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + List commentList = + flowTaskCommentService.getFlowTaskCommentListByMultiInstanceExecId(multiInstanceExecId); + List assigneeList = StrUtil.split(trans.getAssigneeList(), ","); + Set approvedAssigneeSet = commentList.stream() + .map(FlowTaskComment::getCreateLoginName).collect(Collectors.toSet()); + List resultList = new LinkedList<>(); + Map usernameMap = + flowCustomExtFactory.getFlowIdentityExtHelper().mapUserShowNameByLoginName(new HashSet<>(assigneeList)); + for (String assignee : assigneeList) { + JSONObject resultData = new JSONObject(); + resultData.put("assignee", assignee); + resultData.put(SHOW_NAME, usernameMap.get(assignee)); + resultData.put("approved", approvedAssigneeSet.contains(assignee)); + resultList.add(resultData); + } + return ResponseResult.success(resultList); + } + + /** + * 提交多实例加签或减签。 + * NOTE: 白名单接口。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 多实例任务的上一级任务Id。 + * @param newAssignees 加签减签人列表,多个指派人之间逗号分隔。 + * @param isAdd 是否为加签,如果没有该参数,为了保持兼容性,缺省值为true。 + * @return 应答结果。 + */ + @PostMapping("/submitConsign") + public ResponseResult submitConsign( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String newAssignees, + @MyRequestBody Boolean isAdd) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyMultiSign(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + HistoricTaskInstance taskInstance = + verifyResult.getData().getObject("taskInstance", HistoricTaskInstance.class); + Task activeMultiInstanceTask = + verifyResult.getData().getObject(ACTIVE_MULTI_INST_TASK, Task.class); + String multiInstanceExecId = flowApiService.getExecutionVariableStringWithSafe( + activeMultiInstanceTask.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + JSONArray assigneeArray = JSON.parseArray(newAssignees); + if (isAdd == null) { + isAdd = true; + } + if (!isAdd) { + List commentList = + flowTaskCommentService.getFlowTaskCommentListByMultiInstanceExecId(multiInstanceExecId); + if (CollUtil.isNotEmpty(commentList)) { + Set approvedAssigneeSet = commentList.stream() + .map(FlowTaskComment::getCreateLoginName).collect(Collectors.toSet()); + String loginName = this.findExistAssignee(approvedAssigneeSet, assigneeArray); + if (loginName != null) { + errorMessage = "数据验证失败,用户 [" + loginName + "] 已经审批,不能减签该用户!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + } else { + // 避免同一人被重复加签。 + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + Set assigneeSet = new HashSet<>(StrUtil.split(trans.getAssigneeList(), ",")); + String loginName = this.findExistAssignee(assigneeSet, assigneeArray); + if (loginName != null) { + errorMessage = "数据验证失败,用户 [" + loginName + "] 已经是会签人,不能重复指定!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + try { + flowApiService.submitConsign(taskInstance, activeMultiInstanceTask, newAssignees, isAdd); + } catch (FlowOperationException e) { + errorMessage = e.getMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 返回当前用户待办的任务列表。 + * + * @param processDefinitionKey 流程标识。 + * @param processDefinitionName 流程定义名 (模糊查询)。 + * @param taskName 任务名称 (模糊查询)。 + * @param pageParam 分页对象。 + * @return 返回当前用户待办的任务列表。如果指定流程标识,则仅返回该流程的待办任务列表。 + */ + @DisableDataFilter + @PostMapping("/listRuntimeTask") + public ResponseResult> listRuntimeTask( + @MyRequestBody String processDefinitionKey, + @MyRequestBody String processDefinitionName, + @MyRequestBody String taskName, + @MyRequestBody(required = true) MyPageParam pageParam) { + String username = TokenData.takeFromRequest().getLoginName(); + MyPageData pageData = flowApiService.getTaskListByUserName( + username, processDefinitionKey, processDefinitionName, taskName, pageParam); + List flowTaskVoList = flowApiService.convertToFlowTaskList(pageData.getDataList()); + return ResponseResult.success(MyPageUtil.makeResponseData(flowTaskVoList, pageData.getTotalCount())); + } + + /** + * 返回当前用户待办的任务数量。 + * + * @return 返回当前用户待办的任务数量。 + */ + @PostMapping("/countRuntimeTask") + public ResponseResult countRuntimeTask() { + String username = TokenData.takeFromRequest().getLoginName(); + long totalCount = flowApiService.getTaskCountByUserName(username); + return ResponseResult.success(totalCount); + } + + /** + * 主动驳回当前的待办任务到开始节点,只用当前待办任务的指派人或者候选者才能完成该操作。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 待办任务Id。 + * @param taskComment 驳回备注。 + * @return 操作应答结果。 + */ + @PostMapping("/rejectToStartUserTask") + public ResponseResult rejectToStartUserTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String taskComment) { + ResponseResult taskResult = + flowOperationHelper.verifySubmitAndGetTask(processInstanceId, taskId, null); + if (!taskResult.isSuccess()) { + return ResponseResult.errorFrom(taskResult); + } + FlowTaskComment firstTaskComment = flowTaskCommentService.getFirstFlowTaskComment(processInstanceId); + CallResult result = flowApiService.backToRuntimeTask( + taskResult.getData(), firstTaskComment.getTaskKey(), true, taskComment); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 主动驳回当前的待办任务,只用当前待办任务的指派人或者候选者才能完成该操作。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 待办任务Id。 + * @param taskComment 驳回备注。 + * @return 操作应答结果。 + */ + @PostMapping("/rejectRuntimeTask") + public ResponseResult rejectRuntimeTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String taskComment) { + String errorMessage; + ResponseResult taskResult = + flowOperationHelper.verifySubmitAndGetTask(processInstanceId, taskId, null); + if (!taskResult.isSuccess()) { + return ResponseResult.errorFrom(taskResult); + } + CallResult result = flowApiService.backToRuntimeTask(taskResult.getData(), null, true, taskComment); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 撤回当前用户提交的,但是尚未被审批的待办任务。只有已办任务的指派人才能完成该操作。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 待撤回的已办任务Id。 + * @param taskComment 撤回备注。 + * @return 操作应答结果。 + */ + @PostMapping("/revokeHistoricTask") + public ResponseResult revokeHistoricTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String taskComment) { + String errorMessage; + if (!flowApiService.existActiveProcessInstance(processInstanceId)) { + errorMessage = "数据验证失败,当前流程实例已经结束,不能执行撤回!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (taskInstance == null) { + errorMessage = "数据验证失败,当前任务不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(taskInstance.getAssignee(), TokenData.takeFromRequest().getLoginName())) { + errorMessage = "数据验证失败,任务指派人与当前用户不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowTaskComment latestComment = flowTaskCommentService.getLatestFlowTaskComment(processInstanceId); + if (latestComment == null) { + errorMessage = "数据验证失败,当前实例没有任何审批提交记录!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!latestComment.getTaskId().equals(taskId)) { + errorMessage = "数据验证失败,当前审批任务已被办理,不能撤回!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + List activeTaskList = flowApiService.getProcessInstanceActiveTaskList(processInstanceId); + if (CollUtil.isEmpty(activeTaskList)) { + errorMessage = "数据验证失败,当前流程没有任何待办任务!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (latestComment.getApprovalType().equals(FlowApprovalType.TRANSFER)) { + if (activeTaskList.size() > 1) { + errorMessage = "数据验证失败,转办任务数量不能多于1个!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 如果是转办任务,无需节点跳转,将指派人改为当前用户即可。 + Task task = activeTaskList.get(0); + task.setAssignee(TokenData.takeFromRequest().getLoginName()); + } else { + CallResult result = + flowApiService.backToRuntimeTask(activeTaskList.get(0), null, false, taskComment); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + } + return ResponseResult.success(); + } + + /** + * 获取当前流程任务的审批列表。 + * + * @param processInstanceId 当前运行时的流程实例Id。 + * @return 当前流程实例的详情数据。 + */ + @GetMapping("/listFlowTaskComment") + public ResponseResult> listFlowTaskComment(@RequestParam String processInstanceId) { + List flowTaskCommentList = + flowTaskCommentService.getFlowTaskCommentList(processInstanceId); + List resultList = MyModelUtil.copyCollectionTo(flowTaskCommentList, FlowTaskCommentVo.class); + return ResponseResult.success(resultList); + } + + /** + * 获取指定流程定义的流程图。 + * + * @param processDefinitionId 流程定义Id。 + * @return 流程图。 + */ + @GetMapping("/viewProcessBpmn") + public ResponseResult viewProcessBpmn(@RequestParam String processDefinitionId) throws IOException { + BpmnXMLConverter converter = new BpmnXMLConverter(); + BpmnModel bpmnModel = flowApiService.getBpmnModelByDefinitionId(processDefinitionId); + byte[] xmlBytes = converter.convertToXML(bpmnModel); + InputStream in = new ByteArrayInputStream(xmlBytes); + return ResponseResult.success(StreamUtils.copyToString(in, StandardCharsets.UTF_8)); + } + + /** + * 获取流程图高亮数据。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程图高亮数据。 + */ + @GetMapping("/viewHighlightFlowData") + public ResponseResult viewHighlightFlowData(@RequestParam String processInstanceId) { + List activityInstanceList = + flowApiService.getHistoricActivityInstanceList(processInstanceId); + Set finishedTaskSet = activityInstanceList.stream() + .filter(s -> !StrUtil.equals(s.getActivityType(), "sequenceFlow")) + .map(HistoricActivityInstance::getActivityId).collect(Collectors.toSet()); + Set finishedSequenceFlowSet = activityInstanceList.stream() + .filter(s -> StrUtil.equals(s.getActivityType(), "sequenceFlow")) + .map(HistoricActivityInstance::getActivityId).collect(Collectors.toSet()); + //获取流程实例当前正在待办的节点 + List unfinishedInstanceList = + flowApiService.getHistoricUnfinishedInstanceList(processInstanceId); + Set unfinishedTaskSet = new LinkedHashSet<>(); + for (HistoricActivityInstance unfinishedActivity : unfinishedInstanceList) { + unfinishedTaskSet.add(unfinishedActivity.getActivityId()); + } + JSONObject jsonData = new JSONObject(); + jsonData.put("finishedTaskSet", finishedTaskSet); + jsonData.put("finishedSequenceFlowSet", finishedSequenceFlowSet); + jsonData.put("unfinishedTaskSet", unfinishedTaskSet); + return ResponseResult.success(jsonData); + } + + /** + * 获取当前用户的已办理的审批任务列表。 + * + * @param processDefinitionName 流程名。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 查询结果应答。 + */ + @DisableDataFilter + @PostMapping("/listHistoricTask") + public ResponseResult>> listHistoricTask( + @MyRequestBody String processDefinitionName, + @MyRequestBody String beginDate, + @MyRequestBody String endDate, + @MyRequestBody(required = true) MyPageParam pageParam) throws ParseException { + MyPageData pageData = + flowApiService.getHistoricTaskInstanceFinishedList(processDefinitionName, beginDate, endDate, pageParam); + List> resultList = new LinkedList<>(); + pageData.getDataList().forEach(instance -> resultList.add(BeanUtil.beanToMap(instance))); + List taskInstanceList = pageData.getDataList(); + if (CollUtil.isNotEmpty(taskInstanceList)) { + Set instanceIdSet = taskInstanceList.stream() + .map(HistoricTaskInstance::getProcessInstanceId).collect(Collectors.toSet()); + List instanceList = flowApiService.getHistoricProcessInstanceList(instanceIdSet); + Set loginNameSet = instanceList.stream() + .map(HistoricProcessInstance::getStartUserId).collect(Collectors.toSet()); + List userInfoList = flowCustomExtFactory + .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); + Map userInfoMap = + userInfoList.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + Map instanceMap = + instanceList.stream().collect(Collectors.toMap(HistoricProcessInstance::getId, c -> c)); + List workOrderList = + flowWorkOrderService.getInList(INSTANCE_ID, instanceIdSet); + Map workOrderMap = + workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + resultList.forEach(result -> { + String instanceId = result.get(INSTANCE_ID).toString(); + HistoricProcessInstance instance = instanceMap.get(instanceId); + result.put("processDefinitionKey", instance.getProcessDefinitionKey()); + result.put("processDefinitionName", instance.getProcessDefinitionName()); + result.put("startUser", instance.getStartUserId()); + FlowUserInfoVo userInfo = userInfoMap.get(instance.getStartUserId()); + result.put(SHOW_NAME, userInfo.getShowName()); + result.put("headImageUrl", userInfo.getHeadImageUrl()); + result.put("businessKey", instance.getBusinessKey()); + FlowWorkOrder flowWorkOrder = workOrderMap.get(instanceId); + if (flowWorkOrder != null) { + result.put("workOrderCode", flowWorkOrder.getWorkOrderCode()); + } + }); + Set taskIdSet = + taskInstanceList.stream().map(HistoricTaskInstance::getId).collect(Collectors.toSet()); + List commentList = flowTaskCommentService.getFlowTaskCommentListByTaskIds(taskIdSet); + Map> commentMap = + commentList.stream().collect(Collectors.groupingBy(FlowTaskComment::getTaskId)); + resultList.forEach(result -> { + List comments = commentMap.get(result.get("id").toString()); + if (CollUtil.isNotEmpty(comments)) { + result.put("approvalType", comments.get(0).getApprovalType()); + comments.remove(0); + } + }); + } + return ResponseResult.success(MyPageUtil.makeResponseData(resultList, pageData.getTotalCount())); + } + + /** + * 根据输入参数查询,当前用户的历史流程数据。 + * + * @param processDefinitionName 流程名。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 查询结果应答。 + */ + @DisableDataFilter + @PostMapping("/listHistoricProcessInstance") + public ResponseResult>> listHistoricProcessInstance( + @MyRequestBody String processDefinitionName, + @MyRequestBody String beginDate, + @MyRequestBody String endDate, + @MyRequestBody(required = true) MyPageParam pageParam) throws ParseException { + String loginName = TokenData.takeFromRequest().getLoginName(); + MyPageData pageData = flowApiService.getHistoricProcessInstanceList( + null, processDefinitionName, loginName, beginDate, endDate, pageParam, true); + Set loginNameSet = pageData.getDataList().stream() + .map(HistoricProcessInstance::getStartUserId).collect(Collectors.toSet()); + List userInfoList = flowCustomExtFactory + .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); + if (CollUtil.isEmpty(userInfoList)) { + userInfoList = new LinkedList<>(); + } + Map userInfoMap = + userInfoList.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + Set instanceIdSet = pageData.getDataList().stream() + .map(HistoricProcessInstance::getId).collect(Collectors.toSet()); + List workOrderList = + flowWorkOrderService.getInList(INSTANCE_ID, instanceIdSet); + Map workOrderMap = + workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + List> resultList = new LinkedList<>(); + pageData.getDataList().forEach(instance -> { + Map data = BeanUtil.beanToMap(instance); + FlowUserInfoVo userInfo = userInfoMap.get(instance.getStartUserId()); + if (userInfo != null) { + data.put(SHOW_NAME, userInfo.getShowName()); + data.put("headImageUrl", userInfo.getHeadImageUrl()); + } + FlowWorkOrder workOrder = workOrderMap.get(instance.getId()); + if (workOrder != null) { + data.put("workOrderCode", workOrder.getWorkOrderCode()); + data.put("flowStatus", workOrder.getFlowStatus()); + } + resultList.add(data); + }); + return ResponseResult.success(MyPageUtil.makeResponseData(resultList, pageData.getTotalCount())); + } + + /** + * 根据输入参数查询,所有历史流程数据。 + * + * @param processDefinitionName 流程名。 + * @param startUser 流程发起用户。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 查询结果。 + */ + @PostMapping("/listAllHistoricProcessInstance") + public ResponseResult>> listAllHistoricProcessInstance( + @MyRequestBody String processDefinitionName, + @MyRequestBody String startUser, + @MyRequestBody String beginDate, + @MyRequestBody String endDate, + @MyRequestBody(required = true) MyPageParam pageParam) throws ParseException { + MyPageData pageData = flowApiService.getHistoricProcessInstanceList( + null, processDefinitionName, startUser, beginDate, endDate, pageParam, false); + List> resultList = new LinkedList<>(); + pageData.getDataList().forEach(instance -> resultList.add(BeanUtil.beanToMap(instance))); + List unfinishedProcessInstanceIds = pageData.getDataList().stream() + .filter(c -> c.getEndTime() == null) + .map(HistoricProcessInstance::getId) + .collect(Collectors.toList()); + MyPageData> pageResultData = + MyPageUtil.makeResponseData(resultList, pageData.getTotalCount()); + if (CollUtil.isEmpty(unfinishedProcessInstanceIds)) { + return ResponseResult.success(pageResultData); + } + Set processInstanceIds = pageData.getDataList().stream() + .map(HistoricProcessInstance::getId).collect(Collectors.toSet()); + List taskList = flowApiService.getTaskListByProcessInstanceIds(unfinishedProcessInstanceIds); + Map> taskMap = + taskList.stream().collect(Collectors.groupingBy(Task::getProcessInstanceId)); + for (Map result : resultList) { + String processInstanceId = result.get(INSTANCE_ID).toString(); + List instanceTaskList = taskMap.get(processInstanceId); + if (instanceTaskList != null) { + JSONArray taskArray = new JSONArray(); + for (Task task : instanceTaskList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("taskId", task.getId()); + jsonObject.put("taskName", task.getName()); + jsonObject.put("taskKey", task.getTaskDefinitionKey()); + jsonObject.put("assignee", task.getAssignee()); + taskArray.add(jsonObject); + } + result.put("runtimeTaskInfoList", taskArray); + } + } + return ResponseResult.success(pageResultData); + } + + /** + * 催办工单,只有流程发起人才可以催办工单。 + * 催办场景必须要取消数据权限过滤,因为流程的指派很可能是跨越部门的。 + * 既然被指派和催办了,这里就应该禁用工单表的数据权限过滤约束。 + * 如果您的系统没有支持数据权限过滤,DisableDataFilter不会有任何影响,建议保留。 + * + * @param workOrderId 工单Id。 + * @return 应答结果。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.REMIND_TASK) + @PostMapping("/remindRuntimeTask") + public ResponseResult remindRuntimeTask(@MyRequestBody(required = true) Long workOrderId) { + FlowWorkOrder flowWorkOrder = flowWorkOrderService.getById(workOrderId); + if (flowWorkOrder == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMessage; + if (!flowWorkOrder.getCreateUserId().equals(TokenData.takeFromRequest().getUserId())) { + errorMessage = "数据验证失败,只有流程发起人才能催办工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.FINISHED) + || flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.CANCELLED) + || flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.STOPPED)) { + errorMessage = "数据验证失败,已经结束的流程,不能催办工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + errorMessage = "数据验证失败,流程草稿不能催办工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowMessageService.saveNewRemindMessage(flowWorkOrder); + return ResponseResult.success(); + } + + /** + * 取消工作流工单,仅当没有进入任何审批流程之前,才可以取消工单。 + * + * @param workOrderId 工单Id。 + * @param cancelReason 取消原因。 + * @return 应答结果。 + */ + @OperationLog(type = SysOperationLogType.CANCEL_FLOW) + @DisableDataFilter + @PostMapping("/cancelWorkOrder") + public ResponseResult cancelWorkOrder( + @MyRequestBody(required = true) Long workOrderId, + @MyRequestBody(required = true) String cancelReason) { + FlowWorkOrder flowWorkOrder = flowWorkOrderService.getById(workOrderId); + if (flowWorkOrder == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMessage; + if (!flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.SUBMITTED) + && !flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + errorMessage = "数据验证失败,当前流程已经进入审批状态,不能撤销工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowWorkOrder.getCreateUserId().equals(TokenData.takeFromRequest().getUserId())) { + errorMessage = "数据验证失败,当前用户不是工单所有者,不能撤销工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult result; + // 草稿工单直接删除当前工单。 + if (flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + result = flowWorkOrderService.removeDraft(flowWorkOrder); + } else { + result = flowApiService.stopProcessInstance( + flowWorkOrder.getProcessInstanceId(), cancelReason, true); + } + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 获取指定流程定义Id的所有用户任务数据列表。 + * + * @param processDefinitionId 流程定义Id。 + * @return 查询结果。 + */ + @GetMapping("/listAllUserTask") + public ResponseResult> listAllUserTask(@RequestParam String processDefinitionId) { + Map taskMap = flowApiService.getAllUserTaskMap(processDefinitionId); + List resultList = new LinkedList<>(); + for (UserTask t : taskMap.values()) { + JSONObject data = new JSONObject(); + data.put("id", t.getId()); + data.put("name", t.getName()); + resultList.add(data); + } + return ResponseResult.success(resultList); + } + + /** + * 终止流程实例,将任务从当前节点直接流转到主流程的结束事件。 + * + * @param processInstanceId 流程实例Id。 + * @param stopReason 停止原因。 + * @return 执行结果应答。 + */ + @SaCheckPermission("flowOperation.all") + @OperationLog(type = SysOperationLogType.STOP_FLOW) + @DisableDataFilter + @PostMapping("/stopProcessInstance") + public ResponseResult stopProcessInstance( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String stopReason) { + CallResult result = flowApiService.stopProcessInstance(processInstanceId, stopReason, false); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 删除流程实例。 + * + * @param processInstanceId 流程实例Id。 + * @return 执行结果应答。 + */ + @SaCheckPermission("flowOperation.all") + @OperationLog(type = SysOperationLogType.DELETE_FLOW) + @PostMapping("/deleteProcessInstance") + public ResponseResult deleteProcessInstance(@MyRequestBody(required = true) String processInstanceId) { + flowApiService.deleteProcessInstance(processInstanceId); + return ResponseResult.success(); + } + + private List buildApprovedFlowTaskCommentList(TaskInfo taskInfo, boolean isMultiInstanceTask) { + List taskCommentList; + if (isMultiInstanceTask) { + String multiInstanceExecId; + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getByExecutionId(taskInfo.getExecutionId(), taskInfo.getId()); + if (trans != null) { + multiInstanceExecId = trans.getMultiInstanceExecId(); + } else { + multiInstanceExecId = flowApiService.getExecutionVariableStringWithSafe( + taskInfo.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + } + taskCommentList = flowTaskCommentService.getFlowTaskCommentListByMultiInstanceExecId(multiInstanceExecId); + } else { + taskCommentList = flowTaskCommentService.getFlowTaskCommentListByExecutionId( + taskInfo.getProcessInstanceId(), taskInfo.getId(), taskInfo.getExecutionId()); + } + return taskCommentList; + } + + private ResponseResult doVerifyMultiSign(String processInstanceId, String taskId) { + String errorMessage; + if (!flowApiService.existActiveProcessInstance(processInstanceId)) { + errorMessage = "数据验证失败,当前流程实例已经结束,不能执行加签!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (taskInstance == null) { + errorMessage = "数据验证失败,当前任务不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String loginName = TokenData.takeFromRequest().getLoginName(); + if (!StrUtil.equals(taskInstance.getAssignee(), loginName)) { + errorMessage = "数据验证失败,任务指派人与当前用户不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + List activeTaskList = flowApiService.getProcessInstanceActiveTaskList(processInstanceId); + Task activeMultiInstanceTask = null; + Map userTaskMap = flowApiService.getAllUserTaskMap(taskInstance.getProcessDefinitionId()); + for (Task activeTask : activeTaskList) { + UserTask userTask = userTaskMap.get(activeTask.getTaskDefinitionKey()); + if (!userTask.hasMultiInstanceLoopCharacteristics()) { + errorMessage = "数据验证失败,指定加签任务不存在或已审批完毕!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String startTaskId = flowApiService.getTaskVariableStringWithSafe( + activeTask.getId(), FlowConstant.MULTI_SIGN_START_TASK_VAR); + if (StrUtil.equals(startTaskId, taskId)) { + activeMultiInstanceTask = activeTask; + break; + } + } + if (activeMultiInstanceTask == null) { + errorMessage = "数据验证失败,指定加签任务不存在或已审批完毕!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + JSONObject resultData = new JSONObject(); + resultData.put("taskInstance", taskInstance); + resultData.put(ACTIVE_MULTI_INST_TASK, activeMultiInstanceTask); + return ResponseResult.success(resultData); + } + + private String findExistAssignee(Set assigneeSet, JSONArray assigneeArray) { + for (int i = 0; i < assigneeArray.size(); i++) { + String loginName = assigneeArray.getString(i); + if (assigneeSet.contains(loginName)) { + return loginName; + } + } + return null; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java new file mode 100644 index 00000000..7cd964f6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowCategory; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * FlowCategory数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowCategoryMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowCategoryFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowCategoryList( + @Param("flowCategoryFilter") FlowCategory flowCategoryFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java new file mode 100644 index 00000000..3e4154a8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntry; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * FlowEntry数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowEntryFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowEntryList( + @Param("flowEntryFilter") FlowEntry flowEntryFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java new file mode 100644 index 00000000..233c5531 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntryPublish; + +/** + * 数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryPublishMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java new file mode 100644 index 00000000..76de0460 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntryPublishVariable; + +import java.util.List; + +/** + * 数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryPublishVariableMapper extends BaseDaoMapper { + + /** + * 批量插入流程发布的变量列表。 + * + * @param entryPublishVariableList 流程发布的变量列表。 + */ + void insertList(List entryPublishVariableList); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java new file mode 100644 index 00000000..c7c133bb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntryVariable; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 流程变量数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryVariableMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowEntryVariableFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowEntryVariableList( + @Param("flowEntryVariableFilter") FlowEntryVariable flowEntryVariableFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java new file mode 100644 index 00000000..c37279f2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMessageCandidateIdentity; +import org.apache.ibatis.annotations.Param; + +/** + * 流程任务消息的候选身份数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageCandidateIdentityMapper extends BaseDaoMapper { + + /** + * 删除指定流程实例的消息关联数据。 + * + * @param processInstanceId 流程实例Id。 + */ + void deleteByProcessInstanceId(@Param("processInstanceId") String processInstanceId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java new file mode 100644 index 00000000..bc635b07 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMessageIdentityOperation; +import org.apache.ibatis.annotations.Param; + +/** + * 流程任务消息所属用户的操作数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageIdentityOperationMapper extends BaseDaoMapper { + + /** + * 删除指定流程实例的消息关联数据。 + * + * @param processInstanceId 流程实例Id。 + */ + void deleteByProcessInstanceId(@Param("processInstanceId") String processInstanceId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java new file mode 100644 index 00000000..b34474ae --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java @@ -0,0 +1,79 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMessage; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + * 工作流消息数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageMapper extends BaseDaoMapper { + + /** + * 获取指定用户和身份分组Id集合的催办消息列表。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户的登录名。与流程任务的assignee精确匹配。 + * @param groupIdSet 用户身份分组Id集合。 + * @return 查询后的催办消息列表。 + */ + List getRemindingMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet); + + /** + * 获取指定用户和身份分组Id集合的抄送消息列表。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户登录名。 + * @param groupIdSet 用户身份分组Id集合。 + * @param read true表示已读,false表示未读。 + * @return 查询后的抄送消息列表。 + */ + List getCopyMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet, + @Param("read") Boolean read); + + /** + * 计算当前用户催办消息的数量。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户登录名。 + * @param groupIdSet 用户身份分组Id集合。 + * @return 数据数量。 + */ + int countRemindingMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet); + + /** + * 计算当前用户未读抄送消息的数量。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户登录名。 + * @param groupIdSet 用户身份分组Id集合。 + * @return 数据数量 + */ + int countCopyMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java new file mode 100644 index 00000000..131e9368 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMultiInstanceTrans; + +/** + * 流程多实例任务执行流水访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMultiInstanceTransMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java new file mode 100644 index 00000000..5da2bf06 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowTaskComment; + +/** + * 流程任务批注数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskCommentMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java new file mode 100644 index 00000000..9145a5e2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowTaskExt; + +import java.util.List; + +/** + * 流程任务扩展数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskExtMapper extends BaseDaoMapper { + + /** + * 批量插入流程任务扩展信息列表。 + * + * @param flowTaskExtList 流程任务扩展信息列表。 + */ + void insertList(List flowTaskExtList); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java new file mode 100644 index 00000000..b69fd718 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java @@ -0,0 +1,14 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowWorkOrderExt; + +/** + * 工作流工单扩展数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowWorkOrderExtMapper extends BaseDaoMapper { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java new file mode 100644 index 00000000..fe270142 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.annotation.EnableDataPerm; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.*; + +/** + * 工作流工单表数据操作访问接口。 + * 如果当前系统支持数据权限过滤,当前用户必须要能看自己的工单数据,所以需要把EnableDataPerm + * 的mustIncludeUserRule参数设置为true,即便当前用户的数据权限中并不包含DataPermRuleType.TYPE_USER_ONLY, + * 数据过滤拦截组件也会自动补偿该类型的数据权限,以便当前用户可以看到自己发起的工单。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableDataPerm(mustIncludeUserRule = true) +public interface FlowWorkOrderMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowWorkOrderFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowWorkOrderList( + @Param("flowWorkOrderFilter") FlowWorkOrder flowWorkOrderFilter, @Param("orderBy") String orderBy); + + /** + * 计算指定前缀工单编码的最大值。 + * + * @param prefix 工单编码前缀。 + * @return 该工单编码前缀的最大值。 + */ + @Select("SELECT MAX(work_order_code) FROM zz_flow_work_order WHERE work_order_code LIKE '${prefix}'") + String getMaxWorkOrderCodeByPrefix(@Param("prefix") String prefix); + + /** + * 根据工单编码查询指定工单,查询过程也会考虑逻辑删除的数据。 + * @param workOrderCode 工单编码。 + * @return 工单编码的流程工单数量。 + */ + @Select("SELECT COUNT(*) FROM zz_flow_work_order WHERE work_order_code = #{workOrderCode}") + int getCountByWorkOrderCode(@Param("workOrderCode") String workOrderCode); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml new file mode 100644 index 00000000..65460911 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_category.tenant_id IS NULL + + + AND zz_flow_category.tenant_id = #{flowCategoryFilter.tenantId} + + + AND zz_flow_category.app_code IS NULL + + + AND zz_flow_category.app_code = #{flowCategoryFilter.appCode} + + + AND zz_flow_category.name = #{flowCategoryFilter.name} + + + AND zz_flow_category.code = #{flowCategoryFilter.code} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml new file mode 100644 index 00000000..78351d5d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_entry.tenant_id IS NULL + + + AND zz_flow_entry.tenant_id = #{flowEntryFilter.tenantId} + + + AND zz_flow_entry.app_code IS NULL + + + AND zz_flow_entry.app_code = #{flowEntryFilter.appCode} + + + AND zz_flow_entry.process_definition_name = #{flowEntryFilter.processDefinitionName} + + + AND zz_flow_entry.process_definition_key = #{flowEntryFilter.processDefinitionKey} + + + AND zz_flow_entry.category_id = #{flowEntryFilter.categoryId} + + + AND zz_flow_entry.status = #{flowEntryFilter.status} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml new file mode 100644 index 00000000..a8c679aa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml new file mode 100644 index 00000000..68bd83ff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + INSERT INTO zz_flow_entry_publish_variable VALUES + + (#{item.variableId}, + #{item.entryPublishId}, + #{item.variableName}, + #{item.showName}, + #{item.variableType}, + #{item.bindDatasourceId}, + #{item.bindRelationId}, + #{item.bindColumnId}, + #{item.builtin}) + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml new file mode 100644 index 00000000..09a4ea8e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_entry_variable.entry_id = #{flowEntryVariableFilter.entryId} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml new file mode 100644 index 00000000..5dc31fc7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + DELETE FROM zz_flow_msg_candidate_identity a + WHERE EXISTS (SELECT * FROM zz_flow_message b + WHERE a.message_id = b.message_id AND b.process_instance_id = #{processInstanceId}) + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml new file mode 100644 index 00000000..60a8e4a0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + DELETE FROM zz_flow_msg_identity_operation a + WHERE EXISTS (SELECT * FROM zz_flow_message b + WHERE a.message_id = b.message_id AND b.process_instance_id = #{processInstanceId}) + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml new file mode 100644 index 00000000..2fcd87f5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND a.tenant_id IS NULL + + + AND a.tenant_id = #{tenantId} + + + AND a.app_code IS NULL + + + AND a.app_code = #{appCode} + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml new file mode 100644 index 00000000..732758a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml new file mode 100644 index 00000000..69323d82 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml new file mode 100644 index 00000000..2fca8da4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + INSERT INTO zz_flow_task_ext VALUES + + (#{item.processDefinitionId}, + #{item.taskId}, + #{item.operationListJson}, + #{item.variableListJson}, + #{item.assigneeListJson}, + #{item.groupType}, + #{item.deptPostListJson}, + #{item.roleIds}, + #{item.deptIds}, + #{item.candidateUsernames}, + #{item.copyListJson}, + #{item.extraDataJson}) + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml new file mode 100644 index 00000000..2d3867d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml new file mode 100644 index 00000000..24da5a15 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_work_order.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + AND zz_flow_work_order.tenant_id IS NULL + + + AND zz_flow_work_order.tenant_id = #{flowWorkOrderFilter.tenantId} + + + AND zz_flow_work_order.app_code IS NULL + + + AND zz_flow_work_order.app_code = #{flowWorkOrderFilter.appCode} + + + AND zz_flow_work_order.work_order_code = #{flowWorkOrderFilter.workOrderCode} + + + AND zz_flow_work_order.process_definition_key = #{flowWorkOrderFilter.processDefinitionKey} + + + AND zz_flow_work_order.latest_approval_status = #{flowWorkOrderFilter.latestApprovalStatus} + + + AND zz_flow_work_order.flow_status = #{flowWorkOrderFilter.flowStatus} + + + AND zz_flow_work_order.create_time >= #{flowWorkOrderFilter.createTimeStart} + + + AND zz_flow_work_order.create_time <= #{flowWorkOrderFilter.createTimeEnd} + + + AND zz_flow_work_order.create_user_id = #{flowWorkOrderFilter.createUserId} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java new file mode 100644 index 00000000..05b4b875 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.UpdateGroup; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 流程分类的Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程分类的Dto对象") +@Data +public class FlowCategoryDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long categoryId; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称") + @NotBlank(message = "数据验证失败,显示名称不能为空!") + private String name; + + /** + * 分类编码。 + */ + @Schema(description = "分类编码") + @NotBlank(message = "数据验证失败,分类编码不能为空!") + private String code; + + /** + * 实现顺序。 + */ + @Schema(description = "实现顺序") + @NotNull(message = "数据验证失败,实现顺序不能为空!") + private Integer showOrder; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java new file mode 100644 index 00000000..817ae003 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java @@ -0,0 +1,107 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.flow.model.constant.FlowBindFormType; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 流程的Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程的Dto对象") +@Data +public class FlowEntryDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键不能为空!", groups = {UpdateGroup.class}) + private Long entryId; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + @NotBlank(message = "数据验证失败,流程名称不能为空!") + private String processDefinitionName; + + /** + * 流程标识Key。 + */ + @Schema(description = "流程标识Key") + @NotBlank(message = "数据验证失败,流程标识Key不能为空!") + private String processDefinitionKey; + + /** + * 流程分类。 + */ + @Schema(description = "流程分类") + @NotNull(message = "数据验证失败,流程分类不能为空!") + private Long categoryId; + + /** + * 流程状态。 + */ + @Schema(description = "流程状态") + @ConstDictRef(constDictClass = FlowEntryStatus.class, message = "数据验证失败,工作流状态为无效值!") + private Integer status; + + /** + * 流程定义的xml。 + */ + @Schema(description = "流程定义的xml") + private String bpmnXml; + + /** + * 流程图类型。0: 普通流程图,1: 钉钉风格的流程图。 + */ + @Schema(description = "流程图类型。0: 普通流程图,1: 钉钉风格的流程图") + private Integer diagramType; + + /** + * 绑定表单类型。 + */ + @Schema(description = "绑定表单类型") + @ConstDictRef(constDictClass = FlowBindFormType.class, message = "数据验证失败,工作流绑定表单类型为无效值!") + @NotNull(message = "数据验证失败,工作流绑定表单类型不能为空!") + private Integer bindFormType; + + /** + * 在线表单的页面Id。 + */ + @Schema(description = "在线表单的页面Id") + private Long pageId; + + /** + * 在线表单的缺省路由名称。 + */ + @Schema(description = "在线表单的缺省路由名称") + private String defaultRouterName; + + /** + * 在线表单Id。 + */ + @Schema(description = "在线表单Id") + private Long defaultFormId; + + /** + * 工单表编码字段的编码规则,如果为空则不计算工单编码。 + */ + @Schema(description = "工单表编码字段的编码规则") + private String encodedRule; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @Schema(description = "流程的自定义扩展数据") + private String extensionData; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java new file mode 100644 index 00000000..75659d13 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.flow.model.constant.FlowVariableType; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 流程变量Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程变量Dto对象") +@Data +public class FlowEntryVariableDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long variableId; + + /** + * 流程Id。 + */ + @Schema(description = "流程Id") + @NotNull(message = "数据验证失败,流程Id不能为空!") + private Long entryId; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + @NotBlank(message = "数据验证失败,变量名不能为空!") + private String variableName; + + /** + * 显示名。 + */ + @Schema(description = "显示名") + @NotBlank(message = "数据验证失败,显示名不能为空!") + private String showName; + + /** + * 流程变量类型。 + */ + @Schema(description = "流程变量类型") + @ConstDictRef(constDictClass = FlowVariableType.class, message = "数据验证失败,流程变量类型为无效值!") + @NotNull(message = "数据验证失败,流程变量类型不能为空!") + private Integer variableType; + + /** + * 绑定数据源Id。 + */ + @Schema(description = "绑定数据源Id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @Schema(description = "绑定数据源关联Id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @Schema(description = "绑定字段Id") + private Long bindColumnId; + + /** + * 是否内置。 + */ + @Schema(description = "是否内置") + @NotNull(message = "数据验证失败,是否内置不能为空!") + private Boolean builtin; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java new file mode 100644 index 00000000..0d616e97 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java @@ -0,0 +1,51 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 工作流通知消息Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流通知消息Dto对象") +@Data +public class FlowMessageDto { + + /** + * 消息类型。 + */ + @Schema(description = "消息类型") + private Integer messageType; + + /** + * 工单Id。 + */ + @Schema(description = "工单Id") + private Long workOrderId; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程任务名称。 + */ + @Schema(description = "流程任务名称") + private String taskName; + + /** + * 更新时间范围过滤起始值(>=)。 + */ + @Schema(description = "updateTime 范围过滤起始值") + private String updateTimeStart; + + /** + * 更新时间范围过滤结束值(<=)。 + */ + @Schema(description = "updateTime 范围过滤结束值") + private String updateTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java new file mode 100644 index 00000000..4af04f6e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 流程任务的批注。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务的批注") +@Data +public class FlowTaskCommentDto { + + /** + * 流程任务触发按钮类型,内置值可参考FlowTaskButton。 + */ + @Schema(description = "流程任务触发按钮类型") + @NotNull(message = "数据验证失败,任务的审批类型不能为空!") + private String approvalType; + + /** + * 流程任务的批注内容。 + */ + @Schema(description = "流程任务的批注内容") + @NotBlank(message = "数据验证失败,任务审批内容不能为空!") + private String taskComment; + + /** + * 委托指定人,比如加签、转办等。 + */ + @Schema(description = "委托指定人,比如加签、转办等") + private String delegateAssignee; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java new file mode 100644 index 00000000..f87c94c5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 工作流工单Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流工单Dto对象") +@Data +public class FlowWorkOrderDto { + + /** + * 工单编码。 + */ + @Schema(description = "工单编码") + private String workOrderCode; + + /** + * 流程状态。参考FlowTaskStatus常量值对象。 + */ + @Schema(description = "流程状态") + private Integer flowStatus; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @Schema(description = "createTime 范围过滤起始值") + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @Schema(description = "createTime 范围过滤结束值") + private String createTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java new file mode 100644 index 00000000..02784712 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.exception; + +import org.flowable.common.engine.api.FlowableException; + +/** + * 流程空用户异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowEmptyUserException extends FlowableException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public FlowEmptyUserException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java new file mode 100644 index 00000000..313571e1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.flow.exception; + +/** + * 流程操作异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowOperationException extends RuntimeException { + + /** + * 构造函数。 + */ + public FlowOperationException() { + + } + + /** + * 构造函数。 + * + * @param throwable 引发异常对象。 + */ + public FlowOperationException(Throwable throwable) { + super(throwable); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public FlowOperationException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java new file mode 100644 index 00000000..4c1fce9f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java @@ -0,0 +1,165 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.model.FlowTaskExt; +import com.orangeforms.common.flow.object.FlowTaskOperation; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowTaskCommentService; +import com.orangeforms.common.flow.service.FlowTaskExtService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.ExtensionAttribute; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.api.Task; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.*; + +/** + * 流程任务自动审批跳过的监听器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class AutoSkipTaskListener implements TaskListener { + + private final transient FlowTaskCommentService flowTaskCommentService = + ApplicationContextHolder.getBean(FlowTaskCommentService.class); + private final transient FlowApiService flowApiService = + ApplicationContextHolder.getBean(FlowApiService.class); + private final transient FlowTaskExtService flowTaskExtService = + ApplicationContextHolder.getBean(FlowTaskExtService.class); + + /** + * 流程的发起者等于当前任务的Assignee。 + */ + private static final String EQ_START_USER = "0"; + /** + * 上一步的提交者等于当前任务的Assignee。 + */ + private static final String EQ_PREV_SUBMIT_USER = "1"; + /** + * 当前任务的Assignee之前提交过审核。 + */ + private static final String EQ_HISTORIC_SUBMIT_USER = "2"; + + @Override + public void notify(DelegateTask t) { + UserTask userTask = flowApiService.getUserTask(t.getProcessDefinitionId(), t.getTaskDefinitionKey()); + List attributes = userTask.getAttributes().get(FlowConstant.USER_TASK_AUTO_SKIP_KEY); + Set skipTypes = new HashSet<>(StrUtil.split(attributes.get(0).getValue(), ",")); + String assignedUser = this.getAssignedUser(userTask, t.getProcessDefinitionId(), t.getExecutionId()); + if (StrUtil.isBlank(assignedUser)) { + return; + } + for (String skipType : skipTypes) { + if (this.verifyAndHandle(userTask, t, skipType, assignedUser)) { + return; + } + } + } + + private boolean verifyAndHandle(UserTask userTask, DelegateTask task, String skipType, String assignedUser) { + FlowTaskComment comment = null; + switch (skipType) { + case EQ_START_USER: + Object v = task.getVariable(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR); + if (ObjectUtil.equal(v, assignedUser)) { + comment = flowTaskCommentService.getFirstFlowTaskComment(task.getProcessInstanceId()); + } + break; + case EQ_PREV_SUBMIT_USER: + Object v2 = task.getVariable(FlowConstant.SUBMIT_USER_VAR); + if (ObjectUtil.equal(v2, assignedUser)) { + TokenData tokenData = TokenData.takeFromRequest(); + comment = new FlowTaskComment(); + comment.setCreateUserId(tokenData.getUserId()); + comment.setCreateLoginName(tokenData.getLoginName()); + comment.setCreateUsername(tokenData.getShowName()); + } + break; + case EQ_HISTORIC_SUBMIT_USER: + List comments = + flowTaskCommentService.getFlowTaskCommentList(task.getProcessInstanceId()); + List resultComments = new LinkedList<>(); + for (FlowTaskComment c : comments) { + if (StrUtil.equals(c.getCreateLoginName(), assignedUser)) { + resultComments.add(c); + } + } + if (CollUtil.isNotEmpty(resultComments)) { + comment = resultComments.get(0); + } + break; + default: + break; + } + if (comment != null) { + FlowTaskExt flowTaskExt = flowTaskExtService + .getByProcessDefinitionIdAndTaskId(task.getProcessDefinitionId(), userTask.getId()); + JSONObject taskVariableData = new JSONObject(); + if (StrUtil.isNotBlank(flowTaskExt.getOperationListJson())) { + List taskOperationList = + JSONArray.parseArray(flowTaskExt.getOperationListJson(), FlowTaskOperation.class); + taskOperationList.stream() + .filter(op -> op.getType().equals(FlowApprovalType.AGREE)) + .map(FlowTaskOperation::getLatestApprovalStatus).findFirst() + .ifPresent(status -> taskVariableData.put(FlowConstant.LATEST_APPROVAL_STATUS_KEY, status)); + } + Task t = flowApiService.getTaskById(task.getId()); + comment.fillWith(t); + comment.setApprovalType(FlowApprovalType.AGREE); + comment.setTaskComment(StrFormatter.format("自动跳过审批。审批人 [{}], 跳过原因 [{}]。", + userTask.getAssignee(), this.getMessageBySkipType(skipType))); + flowApiService.completeTask(t, comment, taskVariableData); + } + return comment != null; + } + + private String getAssignedUser(UserTask userTask, String processDefinitionId, String executionId) { + String assignedUser = userTask.getAssignee(); + if (StrUtil.isNotBlank(assignedUser)) { + if (assignedUser.startsWith("${") && assignedUser.endsWith("}")) { + String variableName = assignedUser.substring(2, assignedUser.length() - 1); + assignedUser = flowApiService.getExecutionVariableStringWithSafe(executionId, variableName); + } + } else { + FlowTaskExt flowTaskExt = flowTaskExtService + .getByProcessDefinitionIdAndTaskId(processDefinitionId, userTask.getId()); + List candidateUsernames; + if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { + candidateUsernames = Collections.emptyList(); + } else if (!StrUtil.equals(flowTaskExt.getCandidateUsernames(), "${" + FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR + "}")) { + candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); + } else { + String value = flowApiService + .getExecutionVariableStringWithSafe(executionId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); + candidateUsernames = value == null ? null : StrUtil.split(value, ","); + } + if (candidateUsernames != null && candidateUsernames.size() == 1) { + assignedUser = candidateUsernames.get(0); + } + } + return assignedUser; + } + + private String getMessageBySkipType(String skipType) { + return switch (skipType) { + case EQ_PREV_SUBMIT_USER -> "审批人与上一审批节点处理人相同"; + case EQ_START_USER -> "审批人为发起人"; + case EQ_HISTORIC_SUBMIT_USER -> "审批人审批过"; + default -> ""; + }; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java new file mode 100644 index 00000000..7f47ecca --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.flow.listener; + +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.Map; + +/** + * 当用户任务的候选组为本部门领导岗位时,该监听器会在任务创建时,获取当前流程实例发起人的部门领导。 + * 并将其指派为当前任务的候选组。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class DeptPostLeaderListener implements TaskListener { + + @Override + public void notify(DelegateTask delegateTask) { + Map variables = delegateTask.getVariables(); + if (variables.get(FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR) == null) { + delegateTask.setAssignee(variables.get(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR).toString()); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java new file mode 100644 index 00000000..43f7563f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java @@ -0,0 +1,56 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.object.GlobalThreadLocal; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; + +/** + * 流程实例监听器,在流程实例结束的时候,需要完成一些自定义的业务行为。如: + * 1. 更新流程工单表的审批状态字段。 + * 2. 业务数据同步。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class FlowFinishedListener implements ExecutionListener { + + private final transient FlowWorkOrderService flowWorkOrderService = + ApplicationContextHolder.getBean(FlowWorkOrderService.class); + private final transient FlowCustomExtFactory flowCustomExtFactory = + ApplicationContextHolder.getBean(FlowCustomExtFactory.class); + + @Override + public void notify(DelegateExecution execution) { + if (!StrUtil.equals("end", execution.getEventName())) { + return; + } + boolean enabled = GlobalThreadLocal.setDataFilter(false); + try { + String processInstanceId = execution.getProcessInstanceId(); + FlowWorkOrder workOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); + if (workOrder == null) { + return; + } + int flowStatus = FlowTaskStatus.FINISHED; + if (workOrder.getFlowStatus().equals(FlowTaskStatus.CANCELLED) + || workOrder.getFlowStatus().equals(FlowTaskStatus.STOPPED)) { + flowStatus = workOrder.getFlowStatus(); + } + workOrder.setFlowStatus(flowStatus); + // 更新流程工单中的流程状态。 + flowWorkOrderService.updateFlowStatusByProcessInstanceId(processInstanceId, flowStatus); + // 处理在线表单工作流的自定义状态更新。 + flowCustomExtFactory.getOnlineBusinessDataExtHelper().updateFlowStatus(workOrder); + } finally { + GlobalThreadLocal.setDataFilter(enabled); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java new file mode 100644 index 00000000..ba8e09ad --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java @@ -0,0 +1,80 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.model.FlowTaskExt; +import com.orangeforms.common.flow.object.FlowUserTaskExtData; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowTaskExtService; +import com.orangeforms.common.flow.util.BaseFlowNotifyExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.List; + +/** + * 任务进入待办状态时的通知监听器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class FlowTaskNotifyListener implements TaskListener { + + private final transient FlowTaskExtService flowTaskExtService = + ApplicationContextHolder.getBean(FlowTaskExtService.class); + private final transient FlowApiService flowApiService = + ApplicationContextHolder.getBean(FlowApiService.class); + private final transient FlowCustomExtFactory flowCustomExtFactory = + ApplicationContextHolder.getBean(FlowCustomExtFactory.class); + + @Override + public void notify(DelegateTask delegateTask) { + String definitionId = delegateTask.getProcessDefinitionId(); + String instanceId = delegateTask.getProcessInstanceId(); + String taskId = delegateTask.getId(); + String taskKey = delegateTask.getTaskDefinitionKey(); + FlowTaskExt taskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId(definitionId, taskKey); + if (StrUtil.isBlank(taskExt.getExtraDataJson())) { + return; + } + FlowUserTaskExtData extData = JSON.parseObject(taskExt.getExtraDataJson(), FlowUserTaskExtData.class); + if (CollUtil.isEmpty(extData.getFlowNotifyTypeList())) { + return; + } + ProcessInstance instance = flowApiService.getProcessInstance(instanceId); + Object initiator = flowApiService.getProcessInstanceVariable(instanceId, FlowConstant.PROC_INSTANCE_INITIATOR_VAR); + boolean isMultiInstanceTask = flowApiService.isMultiInstanceTask(definitionId, taskKey); + Task task = flowApiService.getProcessInstanceActiveTask(instanceId, taskId); + List userInfoList = + flowTaskExtService.getCandidateUserInfoList(instanceId, taskExt, task, isMultiInstanceTask, false); + if (CollUtil.isEmpty(userInfoList)) { + log.warn("ProcessDefinition [{}] Task [{}] don't find the candidate users for notification.", + instance.getProcessDefinitionName(), task.getName()); + return; + } + BaseFlowNotifyExtHelper helper = flowCustomExtFactory.getFlowNotifyExtHelper(); + Assert.notNull(helper); + for (String notifyType : extData.getFlowNotifyTypeList()) { + FlowTaskVo flowTaskVo = new FlowTaskVo(); + flowTaskVo.setProcessDefinitionId(definitionId); + flowTaskVo.setProcessInstanceId(instanceId); + flowTaskVo.setTaskKey(taskKey); + flowTaskVo.setTaskName(delegateTask.getName()); + flowTaskVo.setTaskId(delegateTask.getId()); + flowTaskVo.setBusinessKey(instance.getBusinessKey()); + flowTaskVo.setProcessInstanceInitiator(initiator.toString()); + helper.doNotify(notifyType, userInfoList, flowTaskVo); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java new file mode 100644 index 00000000..6760fcc4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java @@ -0,0 +1,32 @@ +package com.orangeforms.common.flow.listener; + +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.Map; + +/** + * 流程任务通用监听器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class FlowUserTaskListener implements TaskListener { + + private final transient RuntimeService runtimeService = + ApplicationContextHolder.getBean(RuntimeService.class); + + @Override + public void notify(DelegateTask delegateTask) { + Map variables = delegateTask.getVariables(); + if (variables.get(FlowConstant.DELEGATE_ASSIGNEE_VAR) != null) { + delegateTask.setAssignee(variables.get(FlowConstant.DELEGATE_ASSIGNEE_VAR).toString()); + runtimeService.removeVariableLocal(delegateTask.getExecutionId(), FlowConstant.DELEGATE_ASSIGNEE_VAR); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java new file mode 100644 index 00000000..f29d6cbb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.flow.listener; + +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.Map; + +/** + * 当用户任务的候选组为上级部门领导岗位时,该监听器会在任务创建时,获取当前流程实例发起人的部门领导。 + * 并将其指派为当前任务的候选组。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class UpDeptPostLeaderListener implements TaskListener { + + @Override + public void notify(DelegateTask delegateTask) { + Map variables = delegateTask.getVariables(); + if (variables.get(FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR) == null) { + delegateTask.setAssignee(variables.get(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR).toString()); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java new file mode 100644 index 00000000..4b7144da --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.impl.el.FixedValue; + +/** + * 更新流程的最后审批状态的监听器,目前用于排他网关到任务结束节点的连线上, + * 以便于准确的判断流程实例的最后审批状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class UpdateLatestApprovalStatusListener implements ExecutionListener { + + private FixedValue latestApprovalStatus; + + private final transient FlowWorkOrderService flowWorkOrderService = + ApplicationContextHolder.getBean(FlowWorkOrderService.class); + + public void setAutoStoreVariablesExp(FixedValue approvalStatus) { + this.latestApprovalStatus = approvalStatus; + } + + @Override + public void notify(DelegateExecution execution) { + if (StrUtil.isNotBlank(latestApprovalStatus.getExpressionText())) { + FlowWorkOrder workOrder = + flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(execution.getProcessInstanceId()); + if (workOrder == null) { + return; + } + Integer approvalStatus = Integer.valueOf(latestApprovalStatus.getExpressionText()); + String processInstanceId = execution.getProcessInstanceId(); + flowWorkOrderService.updateLatestApprovalStatusByProcessInstanceId(processInstanceId, approvalStatus); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java new file mode 100644 index 00000000..2b78ff69 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程分类的实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_category") +public class FlowCategory { + + /** + * 主键Id。 + */ + @Id(value = "category_id") + private Long categoryId; + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 显示名称。 + */ + @Column(value = "name") + private String name; + + /** + * 分类编码。 + */ + @Column(value = "code") + private String code; + + /** + * 实现顺序。 + */ + @Column(value = "show_order") + private Integer showOrder; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java new file mode 100644 index 00000000..ceea833f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java @@ -0,0 +1,154 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationOneToOne; +import lombok.Data; + +import java.util.Date; + +/** + * 流程的实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_entry") +public class FlowEntry { + + /** + * 主键。 + */ + @Id(value = "entry_id") + private Long entryId; + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 流程名称。 + */ + @Column(value = "process_definition_name") + private String processDefinitionName; + + /** + * 流程标识Key。 + */ + @Column(value = "process_definition_key") + private String processDefinitionKey; + + /** + * 流程分类。 + */ + @Column(value = "category_id") + private Long categoryId; + + /** + * 工作流部署的发布主版本Id。 + */ + @Column(value = "main_entry_publish_id") + private Long mainEntryPublishId; + + /** + * 最新发布时间。 + */ + @Column(value = "latest_publish_time") + private Date latestPublishTime; + + /** + * 流程状态。 + */ + @Column(value = "status") + private Integer status; + + /** + * 流程定义的xml。 + */ + @Column(value = "bpmn_xml") + private String bpmnXml; + + /** + * 流程图类型。0: 普通流程图,1: 钉钉风格的流程图。 + */ + @Column(value = "diagram_type") + private Integer diagramType; + + /** + * 绑定表单类型。 + */ + @Column(value = "bind_form_type") + private Integer bindFormType; + + /** + * 在线表单的页面Id。 + */ + @Column(value = "page_id") + private Long pageId; + + /** + * 在线表单Id。 + */ + @Column(value = "default_form_id") + private Long defaultFormId; + + /** + * 静态表单的缺省路由名称。 + */ + @Column(value = "default_router_name") + private String defaultRouterName; + + /** + * 工单表编码字段的编码规则,如果为空则不计算工单编码。 + */ + @Column(value = "encoded_rule") + private String encodedRule; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @Column(value = "extension_data") + private String extensionData; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + @Column(ignore = true) + private FlowEntryPublish mainFlowEntryPublish; + + @RelationOneToOne( + masterIdField = "categoryId", + slaveModelClass = FlowCategory.class, + slaveIdField = "categoryId") + @Column(ignore = true) + private FlowCategory flowCategory; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java new file mode 100644 index 00000000..21e4c774 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java @@ -0,0 +1,89 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程发布数据的实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_entry_publish") +public class FlowEntryPublish { + + /** + * 主键Id。 + */ + @Id(value = "entry_publish_id") + private Long entryPublishId; + + /** + * 流程Id。 + */ + @Column(value = "entry_id") + private Long entryId; + + /** + * 流程引擎的部署Id。 + */ + @Column(value = "deploy_id") + private String deployId; + + /** + * 流程引擎中的流程定义Id。 + */ + @Column(value = "process_definition_id") + private String processDefinitionId; + + /** + * 发布版本。 + */ + @Column(value = "publish_version") + private Integer publishVersion; + + /** + * 激活状态。 + */ + @Column(value = "active_status") + private Boolean activeStatus; + + /** + * 是否为主版本。 + */ + @Column(value = "main_version") + private Boolean mainVersion; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 发布时间。 + */ + @Column(value = "publish_time") + private Date publishTime; + + /** + * 第一个非开始节点任务的附加信息。 + */ + @Column(value = "init_task_info") + private String initTaskInfo; + + /** + * 分析后的节点JSON信息。 + */ + @Column(value = "analyzed_node_json") + private String analyzedNodeJson; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @Column(value = "extension_data") + private String extensionData; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java new file mode 100644 index 00000000..da09c1b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * FlowEntryPublishVariable实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_entry_publish_variable") +public class FlowEntryPublishVariable { + + /** + * 主键Id。 + */ + @Id(value = "variable_id") + private Long variableId; + + /** + * 流程Id。 + */ + @Column(value = "entry_publish_id") + private Long entryPublishId; + + /** + * 变量名。 + */ + @Column(value = "variable_name") + private String variableName; + + /** + * 显示名。 + */ + @Column(value = "show_name") + private String showName; + + /** + * 变量类型。 + */ + @Column(value = "variable_type") + private Integer variableType; + + /** + * 是否内置。 + */ + @Column(value = "builtin") + private Boolean builtin; + + /** + * 绑定数据源Id。 + */ + @Column(value = "bind_datasource_id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @Column(value = "bind_relation_id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @Column(value = "bind_column_id") + private Long bindColumnId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java new file mode 100644 index 00000000..1e05bc6a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程变量实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_entry_variable") +public class FlowEntryVariable { + + /** + * 主键Id。 + */ + @Id(value = "variable_id") + private Long variableId; + + /** + * 流程Id。 + */ + @Column(value = "entry_id") + private Long entryId; + + /** + * 变量名。 + */ + @Column(value = "variable_name") + private String variableName; + + /** + * 显示名。 + */ + @Column(value = "show_name") + private String showName; + + /** + * 流程变量类型。 + */ + @Column(value = "variable_type") + private Integer variableType; + + /** + * 绑定数据源Id。 + */ + @Column(value = "bind_datasource_id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @Column(value = "bind_relation_id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @Column(value = "bind_column_id") + private Long bindColumnId; + + /** + * 是否内置。 + */ + @Column(value = "builtin") + private Boolean builtin; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java new file mode 100644 index 00000000..f0c977fe --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java @@ -0,0 +1,167 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 工作流通知消息实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_message") +public class FlowMessage { + + /** + * 主键Id。 + */ + @Id(value = "message_id") + private Long messageId; + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 消息类型。 + */ + @Column(value = "message_type") + private Integer messageType; + + /** + * 消息内容。 + */ + @Column(value = "message_content") + private String messageContent; + + /** + * 催办次数。 + */ + @Column(value = "remind_count") + private Integer remindCount; + + /** + * 工单Id。 + */ + @Column(value = "work_order_id") + private Long workOrderId; + + /** + * 流程定义Id。 + */ + @Column(value = "process_definition_id") + private String processDefinitionId; + + /** + * 流程定义标识。 + */ + @Column(value = "process_definition_key") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @Column(value = "process_definition_name") + private String processDefinitionName; + + /** + * 流程实例Id。 + */ + @Column(value = "process_instance_id") + private String processInstanceId; + + /** + * 流程实例发起者。 + */ + @Column(value = "process_instance_initiator") + private String processInstanceInitiator; + + /** + * 流程任务Id。 + */ + @Column(value = "task_id") + private String taskId; + + /** + * 流程任务定义标识。 + */ + @Column(value = "task_definition_key") + private String taskDefinitionKey; + + /** + * 流程任务名称。 + */ + @Column(value = "task_name") + private String taskName; + + /** + * 创建时间。 + */ + @Column(value = "task_start_time") + private Date taskStartTime; + + /** + * 任务指派人登录名。 + */ + @Column(value = "task_assignee") + private String taskAssignee; + + /** + * 任务是否已完成。 + */ + @Column(value = "task_finished") + private Boolean taskFinished; + + /** + * 业务数据快照。 + */ + @Column(value = "business_data_shot") + private String businessDataShot; + + /** + * 是否为在线表单消息数据。 + */ + @Column(value = "online_form_data") + private Boolean onlineFormData; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 创建者显示名。 + */ + @Column(value = "create_username") + private String createUsername; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java new file mode 100644 index 00000000..c27056f7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 流程任务消息的候选身份实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_msg_candidate_identity") +public class FlowMessageCandidateIdentity { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 任务消息Id。 + */ + @Column(value = "message_id") + private Long messageId; + + /** + * 候选身份类型。 + */ + @Column(value = "candidate_type") + private String candidateType; + + /** + * 候选身份Id。 + */ + @Column(value = "candidate_id") + private String candidateId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java new file mode 100644 index 00000000..f1eb555b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程任务消息所属用户的操作表。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_msg_identity_operation") +public class FlowMessageIdentityOperation { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 任务消息Id。 + */ + @Column(value = "message_id") + private Long messageId; + + /** + * 用户登录名。 + */ + @Column(value = "login_name") + private String loginName; + + /** + * 操作类型。 + * 常量值参考FlowMessageOperationType对象。 + */ + @Column(value = "operation_type") + private Integer operationType; + + /** + * 操作时间。 + */ + @Column(value = "operation_time") + private Date operationTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java new file mode 100644 index 00000000..245af434 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.flowable.task.api.TaskInfo; + +import java.util.Date; + +/** + * 流程多实例任务执行流水对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +@Table(value = "zz_flow_multi_instance_trans") +public class FlowMultiInstanceTrans { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 流程实例Id。 + */ + @Column(value = "process_instance_id") + private String processInstanceId; + + /** + * 任务Id。 + */ + @Column(value = "task_id") + private String taskId; + + /** + * 任务标识。 + */ + @Column(value = "task_key") + private String taskKey; + + /** + * 会签任务的执行Id。 + */ + @Column(value = "multi_instance_exec_id") + private String multiInstanceExecId; + + /** + * 任务的执行Id。 + */ + @Column(value = "execution_id") + private String executionId; + + /** + * 会签指派人列表。 + */ + @Column(value = "assignee_list") + private String assigneeList; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 创建者登录名。 + */ + @Column(value = "create_login_name") + private String createLoginName; + + /** + * 创建者显示名。 + */ + @Column(value = "create_username") + private String createUsername; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + public FlowMultiInstanceTrans(TaskInfo task) { + this.fillWith(task); + } + + public void fillWith(TaskInfo task) { + this.taskId = task.getId(); + this.taskKey = task.getTaskDefinitionKey(); + this.processInstanceId = task.getProcessInstanceId(); + this.executionId = task.getExecutionId(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java new file mode 100644 index 00000000..2d042cc9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java @@ -0,0 +1,150 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.util.ContextUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.flowable.task.api.TaskInfo; + +import java.util.Date; + +/** + * FlowTaskComment实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +@Table(value = "zz_flow_task_comment") +public class FlowTaskComment { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 流程实例Id。 + */ + @Column(value = "process_instance_id") + private String processInstanceId; + + /** + * 任务Id。 + */ + @Column(value = "task_id") + private String taskId; + + /** + * 任务标识。 + */ + @Column(value = "task_key") + private String taskKey; + + /** + * 任务名称。 + */ + @Column(value = "task_name") + private String taskName; + + /** + * 用于驳回和自由跳的目标任务标识。 + */ + @Column(value = "target_task_key") + private String targetTaskKey; + + /** + * 任务的执行Id。 + */ + @Column(value = "execution_id") + private String executionId; + + /** + * 会签任务的执行Id。 + */ + @Column(value = "multi_instance_exec_id") + private String multiInstanceExecId; + + /** + * 审批类型。 + */ + @Column(value = "approval_type") + private String approvalType; + + /** + * 批注内容。 + */ + @Column(value = "task_comment") + private String taskComment; + + /** + * 委托指定人,比如加签、转办等。 + */ + @Column(value = "delegate_assignee") + private String delegateAssignee; + + /** + * 自定义数据。开发者可自行扩展,推荐使用JSON格式数据。 + */ + @Column(value = "custom_business_data") + private String customBusinessData; + + /** + * 审批人头像。 + */ + @Column(value = "head_image_url") + private String headImageUrl; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 创建者登录名。 + */ + @Column(value = "create_login_name") + private String createLoginName; + + /** + * 创建者显示名。 + */ + @Column(value = "create_username") + private String createUsername; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + private static final String REQ_ATTRIBUTE_KEY = "flowTaskComment"; + + public FlowTaskComment(TaskInfo task) { + this.fillWith(task); + } + + public static void setToRequest(FlowTaskComment comment) { + if (ContextUtil.getHttpRequest() != null) { + ContextUtil.getHttpRequest().setAttribute(REQ_ATTRIBUTE_KEY, comment); + } + } + + public static FlowTaskComment getFromRequest() { + if (ContextUtil.getHttpRequest() == null) { + return null; + } + return (FlowTaskComment) ContextUtil.getHttpRequest().getAttribute(REQ_ATTRIBUTE_KEY); + } + + public void fillWith(TaskInfo task) { + this.taskId = task.getId(); + this.taskKey = task.getTaskDefinitionKey(); + this.taskName = task.getName(); + this.processInstanceId = task.getProcessInstanceId(); + this.executionId = task.getExecutionId(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java new file mode 100644 index 00000000..5033e6dc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 流程任务扩展实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_task_ext") +public class FlowTaskExt { + + /** + * 流程引擎的定义Id。 + */ + @Column(value = "process_definition_id") + private String processDefinitionId; + + /** + * 流程引擎任务Id。 + */ + @Column(value = "task_id") + private String taskId; + + /** + * 操作列表JSON。 + */ + @Column(value = "operation_list_json") + private String operationListJson; + + /** + * 变量列表JSON。 + */ + @Column(value = "variable_list_json") + private String variableListJson; + + /** + * 存储多实例的assigneeList的JSON。 + */ + @Column(value = "assignee_list_json") + private String assigneeListJson; + + /** + * 分组类型。 + */ + @Column(value = "group_type") + private String groupType; + + /** + * 保存岗位相关的数据。 + */ + @Column(value = "dept_post_list_json") + private String deptPostListJson; + + /** + * 逗号分隔的角色Id。 + */ + @Column(value = "role_ids") + private String roleIds; + + /** + * 逗号分隔的部门Id。 + */ + @Column(value = "dept_ids") + private String deptIds; + + /** + * 逗号分隔候选用户名。 + */ + @Column(value = "candidate_usernames") + private String candidateUsernames; + + /** + * 抄送相关的数据。 + */ + @Column(value = "copy_list_json") + private String copyListJson; + + /** + * 用户任务的扩展属性,存储为JSON的字符串格式。 + */ + @Column(value = "extra_data_json") + private String extraDataJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java new file mode 100644 index 00000000..d3478bbb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java @@ -0,0 +1,162 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.DeptFilterColumn; +import com.orangeforms.common.core.annotation.UserFilterColumn; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 工作流工单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_work_order") +public class FlowWorkOrder { + + /** + * 主键Id。 + */ + @Id(value = "work_order_id") + private Long workOrderId; + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 工单编码字段。 + */ + @Column(value = "work_order_code") + private String workOrderCode; + + /** + * 流程定义标识。 + */ + @Column(value = "process_definition_key") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @Column(value = "process_definition_name") + private String processDefinitionName; + + /** + * 流程引擎的定义Id。 + */ + @Column(value = "process_definition_id") + private String processDefinitionId; + + /** + * 流程实例Id。 + */ + @Column(value = "process_instance_id") + private String processInstanceId; + + /** + * 在线表单的主表Id。 + */ + @Column(value = "online_table_id") + private Long onlineTableId; + + /** + * 静态表单所使用的数据表名。 + */ + @Column(value = "table_name") + private String tableName; + + /** + * 业务主键值。 + */ + @Column(value = "business_key") + private String businessKey; + + /** + * 最近的审批状态。 + */ + @Column(value = "latest_approval_status") + private Integer latestApprovalStatus; + + /** + * 流程状态。参考FlowTaskStatus常量值对象。 + */ + @Column(value = "flow_status") + private Integer flowStatus; + + /** + * 提交用户登录名称。 + */ + @Column(value = "submit_username") + private String submitUsername; + + /** + * 提交用户所在部门Id。 + */ + @DeptFilterColumn + @Column(value = "dept_id") + private Long deptId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @UserFilterColumn + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @Column(ignore = true) + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @Column(ignore = true) + private String createTimeEnd; + + @RelationConstDict( + masterIdField = "flowStatus", + constantDictClass = FlowTaskStatus.class) + @Column(ignore = true) + private Map flowStatusDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java new file mode 100644 index 00000000..369b017f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.flow.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 工作流工单扩展数据实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_flow_work_order_ext") +public class FlowWorkOrderExt { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 流程工单Id。 + */ + @Column(value = "work_order_id") + private Long workOrderId; + + /** + * 草稿数据。 + */ + @Column(value = "draft_data") + private String draftData; + + /** + * 业务数据。 + */ + @Column(value = "business_data") + private String businessData; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java new file mode 100644 index 00000000..37de6e36 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 工作流绑定表单类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowBindFormType { + + /** + * 在线表单。 + */ + public static final int ONLINE_FORM = 0; + /** + * 路由表单。 + */ + public static final int ROUTER_FORM = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(ONLINE_FORM, "在线表单"); + DICT_MAP.put(ROUTER_FORM, "路由表单"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowBindFormType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java new file mode 100644 index 00000000..826e9895 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 工作流状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowEntryStatus { + + /** + * 未发布。 + */ + public static final int UNPUBLISHED = 0; + /** + * 已发布。 + */ + public static final int PUBLISHED = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(UNPUBLISHED, "未发布"); + DICT_MAP.put(PUBLISHED, "已发布"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowEntryStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java new file mode 100644 index 00000000..6bd62cfd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.model.constant; + +/** + * 工作流消息操作类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowMessageOperationType { + + /** + * 已读操作。 + */ + public static final int READ_FINISHED = 0; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowMessageOperationType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java new file mode 100644 index 00000000..18d41da2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.model.constant; + +/** + * 工作流消息类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowMessageType { + + /** + * 催办消息。 + */ + public static final int REMIND_TYPE = 0; + + /** + * 抄送消息。 + */ + public static final int COPY_TYPE = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowMessageType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java new file mode 100644 index 00000000..f68f49ad --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 流程变量类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowVariableType { + + /** + * 流程实例变量。 + */ + public static final int INSTANCE = 0; + /** + * 任务变量。 + */ + public static final int TASK = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(INSTANCE, "流程实例变量"); + DICT_MAP.put(TASK, "任务变量"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowVariableType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java new file mode 100644 index 00000000..a3277c29 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +/** + * 流程任务的扩展属性。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowElementExtProperty { + + /** + * 最近的审批状态,该值目前仅仅用于流程线元素,即SequenceElement。 + */ + private Integer latestApprovalStatus; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java new file mode 100644 index 00000000..fec564d5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java @@ -0,0 +1,37 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 流程扩展数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowEntryExtensionData { + + /** + * 通知类型。 + */ + private List notifyTypes; + /** + * 流程审批状态字典数据列表。Map的key是id和name。 + */ + private List> approvalStatusDict; + /** + * 级联删除业务数据。 + */ + private Boolean cascadeDeleteBusinessData = false; + /** + * 是否支持流程复活。 + */ + private Boolean supportRevive = false; + /** + * 复活数据保留天数。0表示永久保留。 + */ + private Integer keptReviveDays = 0; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java new file mode 100644 index 00000000..978284c7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; + +/** + * 工作流运行时常用对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowRumtimeObject { + + /** + * 运行时流程实例对象。 + */ + private ProcessInstance instance; + /** + * 运行时流程任务对象。 + */ + private Task task; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java new file mode 100644 index 00000000..55c1388b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +/** + * 表示多实例任务的指派人信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowTaskMultiSignAssign { + + /** + * 指派人类型。参考常量类 UserFilterGroup。 + */ + private String assigneeType; + /** + * 逗号分隔的指派人列表。 + */ + private String assigneeList; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java new file mode 100644 index 00000000..8ad86d88 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +/** + * 流程图中的用户任务操作数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowTaskOperation { + + /** + * 操作Id。 + */ + private String id; + /** + * 操作的标签名。 + */ + private String label; + /** + * 操作类型。 + */ + private String type; + /** + * 显示顺序。 + */ + private Integer showOrder; + /** + * 最后审批状态。 + */ + private Integer latestApprovalStatus; + /** + * 在流程图中定义的多实例会签的指定人员信息。 + */ + private FlowTaskMultiSignAssign multiSignAssignee; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java new file mode 100644 index 00000000..5e954d8f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java @@ -0,0 +1,64 @@ +package com.orangeforms.common.flow.object; + +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.Data; + +import java.util.LinkedList; +import java.util.List; + +/** + * 流程任务岗位候选组数据。仅用于流程任务的候选组类型为岗位时。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowTaskPostCandidateGroup { + + /** + * 唯一值,目前仅前端使用。 + */ + private String id; + /** + * 岗位类型。 + * 1. 所有部门岗位审批变量,值为 (allDeptPost)。 + * 2. 本部门岗位审批变量,值为 (selfDeptPost)。 + * 3. 上级部门岗位审批变量,值为 (upDeptPost)。 + * 4. 任意部门关联的岗位审批变量,值为 (deptPost)。 + */ + private String type; + /** + * 岗位Id。type为(1,2,3)时使用该值。 + */ + private String postId; + /** + * 部门岗位Id。type为(4)时使用该值。 + */ + private String deptPostId; + + public static List buildCandidateGroupList(List groupDataList) { + List candidateGroupList = new LinkedList<>(); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR: + candidateGroupList.add(groupData.getPostId()); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_VAR: + candidateGroupList.add(groupData.getDeptPostId()); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + candidateGroupList.add("${" + FlowConstant.SELF_DEPT_POST_PREFIX + groupData.getPostId() + "}"); + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + candidateGroupList.add("${" + FlowConstant.SIBLING_DEPT_POST_PREFIX + groupData.getPostId() + "}"); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + candidateGroupList.add("${" + FlowConstant.UP_DEPT_POST_PREFIX + groupData.getPostId() + "}"); + break; + default: + break; + } + } + return candidateGroupList; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java new file mode 100644 index 00000000..85d8a7a3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java @@ -0,0 +1,63 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +import java.util.List; + +/** + * 流程用户任务扩展数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowUserTaskExtData { + + public static final String NOTIFY_TYPE_MSG = "message"; + public static final String NOTIFY_TYPE_EMAIL = "email"; + + public static final String TIMEOUT_AUTO_COMPLETE = "autoComplete"; + public static final String TIMEOUT_SEND_MSG = "sendMessage"; + + public static final String EMPTY_USER_TO_ASSIGNEE = "toAssignee"; + public static final String EMPTY_USER_AUTO_REJECT = "autoReject"; + public static final String EMPTY_USER_AUTO_COMPLETE = "autoComplete"; + + /** + * 拒绝后再提交,走重新审批。 + */ + public static final String REJECT_TYPE_REDO = "0"; + /** + * 拒绝后再提交,直接回到驳回前的节点。 + */ + public static final String REJECT_TYPE_BACK_TO_SOURCE = "1"; + + /** + * 任务通知类型列表。 + */ + private List flowNotifyTypeList; + /** + * 拒绝后再次提交的审批类型。 + */ + private String rejectType = REJECT_TYPE_REDO; + /** + * 到期提醒的小时数(从待办任务被创建的时候开始计算)。 + */ + private Integer timeoutHours; + /** + * 任务超时的处理方式。 + */ + private String timeoutHandleWay; + /** + * 默认审批人。 + */ + private String defaultAssignee; + /** + * 空用户审批处理方式。 + */ + private String emptyUserHandleWay; + /** + * 空用户审批时设定的审批人。 + */ + private String emptyUserToAssignee; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java new file mode 100644 index 00000000..892ea587 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java @@ -0,0 +1,568 @@ +package com.orangeforms.common.flow.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.model.FlowTaskExt; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FieldExtension; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; + +import javax.xml.stream.XMLStreamException; +import java.text.ParseException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 流程引擎API的接口封装服务。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowApiService { + + /** + * 启动流程实例。 + * + * @param processDefinitionId 流程定义Id。 + * @param dataId 业务主键Id。 + * @return 新启动的流程实例。 + */ + ProcessInstance start(String processDefinitionId, Object dataId); + + /** + * 完成第一个用户任务。 + * + * @param processInstanceId 流程实例Id。 + * @param flowTaskComment 审批对象。 + * @param taskVariableData 流程任务的变量数据。 + * @return 新完成的任务对象。 + */ + Task takeFirstTask(String processInstanceId, FlowTaskComment flowTaskComment, JSONObject taskVariableData); + + /** + * 启动流程实例,如果当前登录用户为第一个用户任务的指派者,或者Assginee为流程启动人变量时, + * 则自动完成第一个用户任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param dataId 当前流程主表的主键数据。 + * @param flowTaskComment 审批对象。 + * @param taskVariableData 流程任务的变量数据。 + * @return 新启动的流程实例。 + */ + ProcessInstance startAndTakeFirst( + String processDefinitionId, Object dataId, FlowTaskComment flowTaskComment, JSONObject taskVariableData); + + /** + * 多实例加签减签。 + * + * @param startTaskInstance 会签对象的发起任务实例。 + * @param multiInstanceActiveTask 正在执行的多实例任务对象。 + * @param newAssignees 新指派人,多个指派人之间逗号分隔。 + * @param isAdd 是否为加签。 + */ + void submitConsign(HistoricTaskInstance startTaskInstance, Task multiInstanceActiveTask, String newAssignees, boolean isAdd); + + /** + * 完成任务,同时提交审批数据。 + * + * @param task 工作流任务对象。 + * @param flowTaskComment 审批对象。 + * @param taskVariableData 流程任务的变量数据。 + */ + void completeTask(Task task, FlowTaskComment flowTaskComment, JSONObject taskVariableData); + + /** + * 判断当前登录用户是否为流程实例中的用户任务的指派人。或是候选人之一,如果是候选人则拾取该任务并成为指派人。 + * 如果都不是,就会返回具体的错误信息。 + * + * @param task 流程实例中的用户任务。 + * @return 调用结果。 + */ + CallResult verifyAssigneeOrCandidateAndClaim(Task task); + + /** + * 初始化并返回流程实例的变量Map。 + * + * @param processDefinitionId 流程定义Id。 + * @return 初始化后的流程实例变量Map。 + */ + Map initAndGetProcessInstanceVariables(String processDefinitionId); + + /** + * 判断当前登录用户是否为流程实例中的用户任务的指派人。或是候选人之一。 + * + * @param task 流程实例中的用户任务。 + * @return 是返回true,否则false。 + */ + boolean isAssigneeOrCandidate(TaskInfo task); + + /** + * 获取指定流程定义的全部流程节点。 + * + * @param processDefinitionId 流程定义Id。 + * @return 当前流程定义的全部节点集合。 + */ + Collection getProcessAllElements(String processDefinitionId); + + /** + * 判断当前登录用户是否为流程实例的发起人。 + * + * @param processInstanceId 流程实例Id。 + * @return 是返回true,否则false。 + */ + boolean isProcessInstanceStarter(String processInstanceId); + + /** + * 为流程实例设置BusinessKey。 + * + * @param processInstanceId 流程实例Id。 + * @param dataId 通常为主表的主键Id。 + */ + void setBusinessKeyForProcessInstance(String processInstanceId, Object dataId); + + /** + * 判断指定的流程实例Id是否存在。 + * + * @param processInstanceId 流程实例Id。 + * @return 存在返回true,否则false。 + */ + boolean existActiveProcessInstance(String processInstanceId); + + /** + * 获取指定的流程实例对象。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例对象。 + */ + ProcessInstance getProcessInstance(String processInstanceId); + + /** + * 获取指定的流程实例对象。 + * + * @param processDefinitionId 流程定义Id。 + * @param businessKey 业务主键Id。 + * @return 流程实例对象。 + */ + ProcessInstance getProcessInstanceByBusinessKey(String processDefinitionId, String businessKey); + + /** + * 获取流程实例的列表。 + * + * @param processInstanceIdSet 流程实例Id集合。 + * @return 流程实例列表。 + */ + List getProcessInstanceList(Set processInstanceIdSet); + + /** + * 根据流程定义Id查询流程定义对象。 + * + * @param processDefinitionId 流程定义Id。 + * @return 流程定义对象。 + */ + ProcessDefinition getProcessDefinitionById(String processDefinitionId); + + /** + * 根据流程部署Id查询流程定义对象。 + * + * @param deployId 流程部署Id。 + * @return 流程定义对象。 + */ + ProcessDefinition getProcessDefinitionByDeployId(String deployId); + + /** + * 获取流程定义的列表。 + * + * @param processDefinitionIdSet 流程定义Id集合。 + * @return 流程定义列表。 + */ + List getProcessDefinitionList(Set processDefinitionIdSet); + + /** + * 挂起流程定义对象。 + * + * @param processDefinitionId 流程定义Id。 + */ + void suspendProcessDefinition(String processDefinitionId); + + /** + * 激活流程定义对象。 + * + * @param processDefinitionId 流程定义Id。 + */ + void activateProcessDefinition(String processDefinitionId); + + /** + * 获取指定流程定义的BpmnModel。 + * + * @param processDefinitionId 流程定义Id。 + * @return 关联的BpmnModel。 + */ + BpmnModel getBpmnModelByDefinitionId(String processDefinitionId); + + /** + * 判断任务是否为多实例任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param taskKey 流程任务标识。 + * @return true为多实例,否则false。 + */ + boolean isMultiInstanceTask(String processDefinitionId, String taskKey); + + /** + * 设置流程实例的变量集合。 + * + * @param processInstanceId 流程实例Id。 + * @param variableMap 变量名。 + */ + void setProcessInstanceVariables(String processInstanceId, Map variableMap); + + /** + * 获取流程实例的变量。 + * + * @param processInstanceId 流程实例Id。 + * @param variableName 变量名。 + * @return 变量值。 + */ + Object getProcessInstanceVariable(String processInstanceId, String variableName); + + /** + * 获取指定流程实例和任务Id的当前活动任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @return 当前流程实例的活动任务。 + */ + Task getProcessInstanceActiveTask(String processInstanceId, String taskId); + + /** + * 获取指定流程实例的当前活动任务列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 当前流程实例的活动任务。 + */ + List getProcessInstanceActiveTaskList(String processInstanceId); + + /** + * 获取指定流程实例的当前活动任务列表,同时转换为流出任务视图对象列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 当前流程实例的活动任务。 + */ + List getProcessInstanceActiveTaskListAndConvert(String processInstanceId); + + /** + * 根据任务Id,获取当前运行时任务。 + * + * @param taskId 任务Id。 + * @return 运行时任务对象。 + */ + Task getTaskById(String taskId); + + /** + * 获取用户的任务列表。这其中包括当前用户作为指派人和候选人。 + * + * @param username 指派人。 + * @param definitionKey 流程定义的标识。 + * @param definitionName 流程定义名。 + * @param taskName 任务名称。 + * @param pageParam 分页对象。 + * @return 用户的任务列表。 + */ + MyPageData getTaskListByUserName( + String username, String definitionKey, String definitionName, String taskName, MyPageParam pageParam); + + /** + * 获取用户的任务数量。这其中包括当前用户作为指派人和候选人。 + * + * @param username 指派人。 + * @return 用户的任务数量。 + */ + long getTaskCountByUserName(String username); + + /** + * 获取流程实例Id集合的运行时任务列表。 + * + * @param processInstanceIdSet 流程实例Id集合。 + * @return 运行时任务列表。 + */ + List getTaskListByProcessInstanceIds(List processInstanceIdSet); + + /** + * 将流程任务列表数据,转换为前端可以显示的流程对象。 + * + * @param taskList 流程引擎中的任务列表。 + * @return 前端可以显示的流程任务列表。 + */ + List convertToFlowTaskList(List taskList); + + /** + * 添加流程实例结束的监听器。 + * + * @param bpmnModel 流程模型。 + * @param listenerClazz 流程监听器的Class对象。 + */ + void addProcessInstanceEndListener(BpmnModel bpmnModel, Class listenerClazz); + + /** + * 添加流程任务的执行监听器。 + * + * @param flowElement 指定任务节点。 + * @param listenerClazz 执行监听器。 + * @param event 事件。 + * @param fieldExtensions 执行监听器的扩展变量列表。 + */ + void addExecutionListener( + FlowElement flowElement, + Class listenerClazz, + String event, + List fieldExtensions); + + /** + * 添加流程任务创建的任务监听器。 + * + * @param userTask 用户任务。 + * @param listenerClazz 任务监听器。 + */ + void addTaskCreateListener(UserTask userTask, Class listenerClazz); + + /** + * 获取流程实例的历史流程实例。 + * + * @param processInstanceId 流程实例Id。 + * @return 历史流程实例。 + */ + HistoricProcessInstance getHistoricProcessInstance(String processInstanceId); + + /** + * 获取流程实例的历史流程实例列表。 + * + * @param processInstanceIdSet 流程实例Id集合。 + * @return 历史流程实例列表。 + */ + List getHistoricProcessInstanceList(Set processInstanceIdSet); + + /** + * 查询历史流程实例的列表。 + * + * @param processDefinitionKey 流程标识名。 + * @param processDefinitionName 流程名。 + * @param startUser 流程发起用户。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @param finishedOnly 仅仅返回已经结束的流程。 + * @return 分页后的查询列表对象。 + * @throws ParseException 日期参数解析失败。 + */ + MyPageData getHistoricProcessInstanceList( + String processDefinitionKey, + String processDefinitionName, + String startUser, + String beginDate, + String endDate, + MyPageParam pageParam, + boolean finishedOnly) throws ParseException; + + /** + * 获取流程实例的已完成历史任务列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例已完成的历史任务列表。 + */ + List getHistoricActivityInstanceList(String processInstanceId); + + /** + * 获取流程实例的已完成历史任务列表,同时按照每个活动实例的开始时间升序排序。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例已完成的历史任务列表。 + */ + List getHistoricActivityInstanceListOrderByStartTime(String processInstanceId); + + /** + * 获取当前用户的历史已办理任务列表。 + * + * @param processDefinitionName 流程名。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 分页后的查询列表对象。 + * @throws ParseException 日期参数解析失败。 + */ + MyPageData getHistoricTaskInstanceFinishedList( + String processDefinitionName, + String beginDate, + String endDate, + MyPageParam pageParam) throws ParseException; + + /** + * 获取指定的历史任务实例。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 任务Id。 + * @return 历史任务实例。 + */ + HistoricTaskInstance getHistoricTaskInstance(String processInstanceId, String taskId); + + /** + * 获取流程实例的待完成任务列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例待完成的任务列表。 + */ + List getHistoricUnfinishedInstanceList(String processInstanceId); + + /** + * 终止流程实例,将任务从当前节点直接流转到主流程的结束事件。 + * + * @param processInstanceId 流程实例Id。 + * @param stopReason 停止原因。 + * @param forCancel 是否由取消工单触发。 + * @return 执行结果。 + */ + CallResult stopProcessInstance(String processInstanceId, String stopReason, boolean forCancel); + + /** + * 终止流程实例,将任务从当前节点直接流转到主流程的结束事件。 + * + * @param processInstanceId 流程实例Id。 + * @param stopReason 停止原因。 + * @param status 流程状态。 + * @return 执行结果。 + */ + CallResult stopProcessInstance(String processInstanceId, String stopReason, int status); + + /** + * 删除流程实例。 + * + * @param processInstanceId 流程实例Id。 + */ + void deleteProcessInstance(String processInstanceId); + + /** + * 获取任务的指定本地变量。 + * + * @param taskId 任务Id。 + * @param variableName 变量名。 + * @return 变量值。 + */ + Object getTaskVariable(String taskId, String variableName); + + /** + * 安全的获取任务变量,并返回字符型的变量值。 + * + * @param taskId 任务Id。 + * @param variableName 变量名。 + * @return 返回变量值的字符串形式,如果变量不存在不会抛异常,返回null。 + */ + String getTaskVariableStringWithSafe(String taskId, String variableName); + + /** + * 获取任务执行时的指定本地变量。 + * + * @param executionId 任务执行时Id。 + * @param variableName 变量名。 + * @return 变量值。 + */ + Object getExecutionVariable(String executionId, String variableName); + + /** + * 安全的获取任务执行时变量,并返回字符型的变量值。 + * + * @param executionId 任务执行时Id。 + * @param variableName 变量名。 + * @return 返回变量值的字符串形式,如果变量不存在不会抛异常,返回null。 + */ + String getExecutionVariableStringWithSafe(String executionId, String variableName); + + /** + * 获取历史流程变量。 + * + * @param processInstanceId 流程实例Id。 + * @param variableName 变量名。 + * @return 获取历史流程变量。 + */ + Object getHistoricProcessInstanceVariable(String processInstanceId, String variableName); + + /** + * 将xml格式的流程模型字符串,转换为标准的流程模型。 + * + * @param bpmnXml xml格式的流程模型字符串。 + * @return 转换后的标准的流程模型。 + * @throws XMLStreamException XML流处理异常 + */ + BpmnModel convertToBpmnModel(String bpmnXml) throws XMLStreamException; + + /** + * 回退到上一个用户任务节点。如果没有指定,则回退到上一个任务。 + * + * @param task 当前活动任务。 + * @param targetKey 指定回退到的任务标识。如果为null,则回退到上一个任务。 + * @param forReject true表示驳回,false为撤回。 + * @param reason 驳回或者撤销的原因。 + * @return 回退结果。 + */ + CallResult backToRuntimeTask(Task task, String targetKey, boolean forReject, String reason); + + /** + * 转办任务给他人。 + * + * @param task 流程任务。 + * @param flowTaskComment 审批对象。 + */ + void transferTo(Task task, FlowTaskComment flowTaskComment); + + /** + * 获取当前任务在流程图中配置候选用户组数据。 + * + * @param flowTaskExt 流程任务扩展对象。 + * @param taskId 运行时任务Id。 + * @return 候选用户组数据。 + */ + List getCandidateUsernames(FlowTaskExt flowTaskExt, String taskId); + + /** + * 获取当前任务在流程图中配置到的部门岗位Id集合和岗位Id集合。 + * + * @param flowTaskExt 流程任务扩展对象。 + * @param processInstanceId 流程实例Id。 + * @param historic 是否为历史任务。 + * @return first为部门岗位Id集合,second是岗位Id集合。 + */ + Tuple2, Set> getDeptPostIdAndPostIds( + FlowTaskExt flowTaskExt, String processInstanceId, boolean historic); + + /** + * 获取流程图中所有用户任务的映射。 + * + * @param processDefinitionId 流程定义Id。 + * @return 流程图中所有用户任务的映射。 + */ + Map getAllUserTaskMap(String processDefinitionId); + + /** + * 获取流程图中指定的用户任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param taskKey 用户任务标识。 + * @return 用户任务。 + */ + UserTask getUserTask(String processDefinitionId, String taskKey); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java new file mode 100644 index 00000000..506c6f15 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.*; + +import java.util.List; + +/** + * FlowCategory数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowCategoryService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowCategory 新增对象。 + * @return 返回新增对象。 + */ + FlowCategory saveNew(FlowCategory flowCategory); + + /** + * 更新数据对象。 + * + * @param flowCategory 更新的对象。 + * @param originalFlowCategory 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(FlowCategory flowCategory, FlowCategory originalFlowCategory); + + /** + * 删除指定数据。 + * + * @param categoryId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long categoryId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowCategoryListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowCategoryList(FlowCategory filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowCategoryList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowCategoryListWithRelation(FlowCategory filter, String orderBy); + + /** + * 当前流程分类编码是否存在。 + * + * @param code 流程分类编码。 + * @return true存在,否则false。 + */ + boolean existByCode(String code); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java new file mode 100644 index 00000000..9cd3a366 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java @@ -0,0 +1,133 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.*; + +import javax.xml.stream.XMLStreamException; +import java.util.List; +import java.util.Set; + +/** + * FlowEntry数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowEntry 新增工作流对象。 + * @return 返回新增对象。 + */ + FlowEntry saveNew(FlowEntry flowEntry); + + /** + * 发布指定流程。 + * + * @param flowEntry 待发布的流程对象。 + * @param initTaskInfo 第一个非开始节点任务的附加信息。 + * @throws XMLStreamException 解析bpmn.xml的异常。 + */ + void publish(FlowEntry flowEntry, String initTaskInfo) throws XMLStreamException; + + /** + * 更新数据对象。 + * + * @param flowEntry 更新的对象。 + * @param originalFlowEntry 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(FlowEntry flowEntry, FlowEntry originalFlowEntry); + + /** + * 删除指定数据。 + * + * @param entryId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long entryId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowEntryListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryList(FlowEntry filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowEntryList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryListWithRelation(FlowEntry filter, String orderBy); + + /** + * 根据流程定义标识获取流程对象。从缓存中读取,如不存在则从数据库读取后,再同步到缓存。 + * + * @param processDefinitionKey 流程定义标识。 + * @return 流程对象。 + */ + FlowEntry getFlowEntryFromCache(String processDefinitionKey); + + /** + * 根据流程Id获取流程发布列表数据。 + * + * @param entryId 流程Id。 + * @return 流程关联的发布列表数据。 + */ + List getFlowEntryPublishList(Long entryId); + + /** + * 根据流程引擎中的流程定义Id集合,查询流程发布对象。 + * + * @param processDefinitionIdSet 流程引擎中的流程定义Id集合。 + * @return 查询结果。 + */ + List getFlowEntryPublishList(Set processDefinitionIdSet); + + /** + * 获取指定工作流发布版本对象。从缓存中读取,如缓存中不存在,从数据库读取并同步缓存。 + * + * @param entryPublishId 工作流发布对象Id。 + * @return 查询后的对象。 + */ + FlowEntryPublish getFlowEntryPublishFromCache(Long entryPublishId); + + /** + * 为指定工作流更新发布的主版本。 + * + * @param flowEntry 工作流对象。 + * @param newMainFlowEntryPublish 工作流新的发布主版本对象。 + */ + void updateFlowEntryMainVersion(FlowEntry flowEntry, FlowEntryPublish newMainFlowEntryPublish); + + /** + * 挂起指定的工作流发布对象。 + * + * @param flowEntryPublish 待挂起的工作流发布对象。 + */ + void suspendFlowEntryPublish(FlowEntryPublish flowEntryPublish); + + /** + * 激活指定的工作流发布对象。 + * + * @param flowEntryPublish 待恢复的工作流发布对象。 + */ + void activateFlowEntryPublish(FlowEntryPublish flowEntryPublish); + + /** + * 判断指定流程定义标识是否存在。 + * @param processDefinitionKey 流程定义标识。 + * @return true存在,否则false。 + */ + boolean existByProcessDefinitionKey(String processDefinitionKey); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java new file mode 100644 index 00000000..963a33fb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 流程变量数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryVariableService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowEntryVariable 新增对象。 + * @return 返回新增对象。 + */ + FlowEntryVariable saveNew(FlowEntryVariable flowEntryVariable); + + /** + * 更新数据对象。 + * + * @param flowEntryVariable 更新的对象。 + * @param originalFlowEntryVariable 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(FlowEntryVariable flowEntryVariable, FlowEntryVariable originalFlowEntryVariable); + + /** + * 删除指定数据。 + * + * @param variableId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long variableId); + + /** + * 删除指定流程Id的所有变量。 + * + * @param entryId 流程Id。 + */ + void removeByEntryId(Long entryId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowEntryVariableListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryVariableList(FlowEntryVariable filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowEntryVariableList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryVariableListWithRelation(FlowEntryVariable filter, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java new file mode 100644 index 00000000..1d0b53f8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java @@ -0,0 +1,106 @@ +package com.orangeforms.common.flow.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.FlowMessage; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import org.flowable.task.api.Task; + +import java.util.List; + +/** + * 工作流消息数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowMessage 新增对象。 + * @return 保存后的消息对象。 + */ + FlowMessage saveNew(FlowMessage flowMessage); + + /** + * 根据工单参数,保存催单消息对象。如果当前工单存在多个待办任务,则插入多条催办消息数据。 + * + * @param flowWorkOrder 待催办的工单。 + */ + void saveNewRemindMessage(FlowWorkOrder flowWorkOrder); + + /** + * 保存抄送消息对象。 + * + * @param task 待抄送的任务。 + * @param copyDataJson 抄送人员或者组的Id数据。 + */ + void saveNewCopyMessage(Task task, JSONObject copyDataJson); + + /** + * 更新指定运行时任务Id的消费为已完成状态。 + * + * @param taskId 运行时任务Id。 + */ + void updateFinishedStatusByTaskId(String taskId); + + /** + * 更新指定流程实例Id的消费为已完成状态。 + * + * @param processInstanceId 流程实例IdId。 + */ + void updateFinishedStatusByProcessInstanceId(String processInstanceId); + + /** + * 获取当前用户的催办消息列表。 + * + * @return 查询后的催办消息列表。 + */ + List getRemindingMessageListByUser(); + + /** + * 获取当前用户的抄送消息列表。 + * + * @param read true表示已读,false表示未读。 + * @return 查询后的抄送消息列表。 + */ + List getCopyMessageListByUser(Boolean read); + + /** + * 判断当前用户是否有权限访问指定消息Id。 + * + * @param messageId 消息Id。 + * @return true为合法访问者,否则false。 + */ + boolean isCandidateIdentityOnMessage(Long messageId); + + /** + * 读取抄送消息,同时更新当前用户对指定抄送消息的读取状态。 + * + * @param messageId 消息Id。 + */ + void readCopyTask(Long messageId); + + /** + * 计算当前用户催办消息的数量。 + * + * @return 当前用户催办消息数量。 + */ + int countRemindingMessageListByUser(); + + /** + * 计算当前用户未读抄送消息的数量。 + * + * @return 当前用户未读抄送消息数量。 + */ + int countCopyMessageByUser(); + + /** + * 删除指定流程实例的消息。 + * + * @param processInstanceId 流程实例Id。 + */ + void removeByProcessInstanceId(String processInstanceId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java new file mode 100644 index 00000000..3b0ff74c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.FlowMultiInstanceTrans; + +/** + * 会签任务操作流水数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMultiInstanceTransService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowMultiInstanceTrans 新增对象。 + * @return 返回新增对象。 + */ + FlowMultiInstanceTrans saveNew(FlowMultiInstanceTrans flowMultiInstanceTrans); + + /** + * 根据流程执行Id获取对象。 + * + * @param executionId 流程执行Id。 + * @param taskId 执行任务Id。 + * @return 数据对象。 + */ + FlowMultiInstanceTrans getByExecutionId(String executionId, String taskId); + + /** + * 根据多实例的统一执行Id,获取assgineeList字段不为空的数据。 + * + * @param multiInstanceExecId 多实例统一执行Id。 + * @return 数据对象。 + */ + FlowMultiInstanceTrans getWithAssigneeListByMultiInstanceExecId(String multiInstanceExecId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java new file mode 100644 index 00000000..7e90388f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 流程任务批注数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskCommentService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowTaskComment 新增对象。 + * @return 返回新增对象。 + */ + FlowTaskComment saveNew(FlowTaskComment flowTaskComment); + + /** + * 查询指定流程实例Id下的所有审批任务的批注。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果集。 + */ + List getFlowTaskCommentList(String processInstanceId); + + /** + * 查询与指定流程任务Id集合关联的所有审批任务的批注。 + * + * @param taskIdSet 流程任务Id集合。 + * @return 查询结果集。 + */ + List getFlowTaskCommentListByTaskIds(Set taskIdSet); + + /** + * 获取指定流程实例的最后一条审批任务。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果。 + */ + FlowTaskComment getLatestFlowTaskComment(String processInstanceId); + + /** + * 获取指定流程实例和任务定义标识的最后一条审批任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskDefinitionKey 任务定义标识。 + * @return 查询结果。 + */ + FlowTaskComment getLatestFlowTaskComment(String processInstanceId, String taskDefinitionKey); + + /** + * 获取指定流程实例的第一条审批任务。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果。 + */ + FlowTaskComment getFirstFlowTaskComment(String processInstanceId); + + /** + * 获取指定任务实例和执行批次的审批数据列表。 + * + * @param processInstanceId 流程实例。 + * @param taskId 任务Id + * @param executionId 任务执行Id + * @return 审批数据列表。 + */ + List getFlowTaskCommentListByExecutionId( + String processInstanceId, String taskId, String executionId); + + /** + * 根据多实例执行Id获取任务审批对象数据列表。 + * + * @param multiInstanceExecId 多实例执行Id。 + * @return 审批数据列表。 + */ + List getFlowTaskCommentListByMultiInstanceExecId(String multiInstanceExecId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java new file mode 100644 index 00000000..dca29c00 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java @@ -0,0 +1,124 @@ +package com.orangeforms.common.flow.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.object.FlowElementExtProperty; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.common.core.base.service.IBaseService; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.ExtensionElement; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.task.api.TaskInfo; + +import java.util.List; +import java.util.Map; + +/** + * 流程任务扩展数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskExtService extends IBaseService { + + /** + * 批量插入流程任务扩展信息列表。 + * + * @param flowTaskExtList 流程任务扩展信息列表。 + */ + void saveBatch(List flowTaskExtList); + + /** + * 查询指定的流程任务扩展对象。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @param taskId 流程引擎的任务Id。 + * @return 查询结果。 + */ + FlowTaskExt getByProcessDefinitionIdAndTaskId(String processDefinitionId, String taskId); + + /** + * 查询指定的流程定义的任务扩展对象。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @return 查询结果。 + */ + List getByProcessDefinitionId(String processDefinitionId); + + /** + * 获取任务扩展信息中的候选人用户信息列表。 + * + * @param processInstanceId 流程引擎的实例Id。 + * @param flowTaskExt 任务扩展对象。 + * @param taskInfo 任务信息。 + * @param isMultiInstanceTask 是否为多实例任务。 + * @param historic 是否为历史任务。 + * @return 候选人用户信息列表。 + */ + List getCandidateUserInfoList( + String processInstanceId, + FlowTaskExt flowTaskExt, + TaskInfo taskInfo, + boolean isMultiInstanceTask, + boolean historic); + + /** + * 获取指定任务的用户列表信息。 + * + * @param processInstanceId 流程实例。 + * @param executionId 执行实例。 + * @param flowTaskExt 流程用户任务的扩展对象。 + * @return 候选人用户信息列表。 + */ + List getCandidateUserInfoList( + String processInstanceId, + String executionId, + FlowTaskExt flowTaskExt); + + /** + * 通过UserTask对象中的扩展节点信息,构建FLowTaskExt对象。 + * + * @param userTask 流程图中定义的用户任务对象。 + * @return 构建后的流程任务扩展信息对象。 + */ + FlowTaskExt buildTaskExtByUserTask(UserTask userTask); + + /** + * 获取指定流程图中所有UserTask对象的扩展节点信息,构建FLowTaskExt对象列表。 + * + * @param bpmnModel 流程图模型对象。 + * @return 当前流程图中所有用户流程任务的扩展信息对象列表。 + */ + List buildTaskExtList(BpmnModel bpmnModel); + + /** + * 根据流程定义中用户任务的扩展节点数据,构建出前端所需的操作列表数据对象。 + * @param extensionMap 用户任务的扩展节点。 + * @return 前端所需的操作列表数据对象。 + */ + List buildOperationListExtensionElement(Map> extensionMap); + + /** + * 根据流程定义中用户任务的扩展节点数据,构建出前端所需的变量列表数据对象。 + * @param extensionMap 用户任务的扩展节点。 + * @return 前端所需的变量列表数据对象。 + */ + List buildVariableListExtensionElement(Map> extensionMap); + + /** + * 读取流程定义中,流程元素的扩展属性数据。 + * + * @param element 流程图中定义的流程元素。 + * @return 流程元素的扩展属性数据。 + */ + FlowElementExtProperty buildFlowElementExt(FlowElement element); + + /** + * 读取流程定义中,流程元素的扩展属性数据。 + * + * @param element 流程图中定义的流程元素。 + * @return 流程元素的扩展属性数据,并转换为JSON对象。 + */ + JSONObject buildFlowElementExtToJson(FlowElement element); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java new file mode 100644 index 00000000..4299abe7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java @@ -0,0 +1,184 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.FlowWorkOrderExt; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import org.flowable.engine.runtime.ProcessInstance; + +import java.util.*; + +/** + * 工作流工单表数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowWorkOrderService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param instance 流程实例对象。 + * @param dataId 流程实例的BusinessKey。 + * @param onlineTableId 在线数据表的主键Id。 + * @param tableName 面向静态表单所使用的表名。 + * @return 新增的工作流工单对象。 + */ + FlowWorkOrder saveNew(ProcessInstance instance, Object dataId, Long onlineTableId, String tableName); + + /** + * 保存工单草稿。 + * + * @param instance 流程实例对象。 + * @param onlineTableId 在线表单的主表Id。 + * @param tableName 静态表单的主表表名。 + * @param masterData 主表数据。 + * @param slaveData 从表数据。 + * @return 工单对象。 + */ + FlowWorkOrder saveNewWithDraft( + ProcessInstance instance, Long onlineTableId, String tableName, String masterData, String slaveData); + + /** + * 更新流程工单的草稿数据。 + * + * @param workOrderId 工单Id。 + * @param masterData 主表数据。 + * @param slaveData 从表数据。 + */ + void updateDraft(Long workOrderId, String masterData, String slaveData); + + /** + * 删除指定数据。 + * + * @param workOrderId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long workOrderId); + + /** + * 删除指定流程实例Id的关联工单。 + * + * @param processInstanceId 流程实例Id。 + */ + void removeByProcessInstanceId(String processInstanceId); + + /** + * 获取工作流工单单表查询结果。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowWorkOrderList(FlowWorkOrder filter, String orderBy); + + /** + * 获取工作流工单列表及其关联字典数据。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowWorkOrderListWithRelation(FlowWorkOrder filter, String orderBy); + + /** + * 根据流程实例Id,查询关联的工单对象。 + * + * @param processInstanceId 流程实例Id。 + * @return 工作流工单对象。 + */ + FlowWorkOrder getFlowWorkOrderByProcessInstanceId(String processInstanceId); + + /** + * 根据业务主键,查询是否存在指定的工单。 + * + * @param tableName 静态表单工作流使用的数据表。 + * @param businessKey 业务数据主键Id。 + * @param unfinished 是否为没有结束工单。 + * @return 存在返回true,否则false。 + */ + boolean existByBusinessKey(String tableName, Object businessKey, boolean unfinished); + + /** + * 根据流程定义和业务主键,查询是否存在指定的未完成工单。 + * + * @param processDefinitionKey 静态表单工作流使用的数据表。 + * @param businessKey 业务数据主键Id。 + * @return 存在返回true,否则false。 + */ + boolean existUnfinished(String processDefinitionKey, Object businessKey); + + /** + * 根据流程实例Id,更新流程状态。 + * + * @param processInstanceId 流程实例Id。 + * @param flowStatus 新的流程状态值,如果该值为null,不执行任何更新。 + */ + void updateFlowStatusByProcessInstanceId(String processInstanceId, Integer flowStatus); + + /** + * 根据流程实例Id,更新流程最后审批状态。 + * + * @param processInstanceId 流程实例Id。 + * @param approvalStatus 新的流程最后审批状态,如果该值为null,不执行任何更新。 + */ + void updateLatestApprovalStatusByProcessInstanceId(String processInstanceId, Integer approvalStatus); + + /** + * 是否有查看该工单的数据权限。 + * + * @param processInstanceId 流程实例Id。 + * @return 存在返回true,否则false。 + */ + boolean hasDataPermOnFlowWorkOrder(String processInstanceId); + + /** + * 根据工单列表中的submitUserName,找到映射的userShowName,并会写到Vo中指定字段。 + * 同时这也是一个如何通过插件方法,将loginName映射到showName的示例, + * + * @param dataList 工单Vo对象列表。 + */ + void fillUserShowNameByLoginName(List dataList); + + /** + * 根据工单Id获取工单扩展对象数据。 + * + * @param workOrderId 工单Id。 + * @return 工单扩展对象。 + */ + FlowWorkOrderExt getFlowWorkOrderExtByWorkOrderId(Long workOrderId); + + /** + * 根据工单Id集合获取工单扩展对象数据列表。 + * + * @param workOrderIds 工单Id集合。 + * @return 工单扩展对象列表。 + */ + List getFlowWorkOrderExtByWorkOrderIds(Set workOrderIds); + + /** + * 移除草稿工单,同时停止已经启动的流程实例。 + * + * @param flowWorkOrder 工单对象。 + * @return 停止流程实例的结果。 + */ + CallResult removeDraft(FlowWorkOrder flowWorkOrder); + + /** + * 获取分页后的工单列表同时构建部分任务数据。该方法主要是为了尽量减少路由表单工作流listWorkOrder的重复代码。 + * + * @param filter 工单过滤对象。 + * @param pageParam 分页参数对象。 + * @param orderParam 排序参数对象。 + * @param processDefinitionKey 流程定义标识。 + * @return 分页的工单列表。 + */ + MyPageData getPagedWorkOrderListAndBuildData( + FlowWorkOrderDto filter, MyPageParam pageParam, MyOrderParam orderParam, String processDefinitionKey); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java new file mode 100644 index 00000000..164d5486 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java @@ -0,0 +1,2032 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.*; +import cn.hutool.core.convert.Convert; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MultiDatabaseWriteMethod; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.UserFilterGroup; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyDateUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.object.*; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.CustomChangeActivityStateBuilderImpl; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; +import org.flowable.common.engine.impl.de.odysseus.el.ExpressionFactoryImpl; +import org.flowable.common.engine.impl.de.odysseus.el.util.SimpleContext; +import org.flowable.common.engine.impl.identity.Authentication; +import org.flowable.common.engine.impl.javax.el.ExpressionFactory; +import org.flowable.common.engine.impl.javax.el.ValueExpression; +import org.flowable.engine.*; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.history.*; +import org.flowable.engine.impl.RuntimeServiceImpl; +import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; +import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ChangeActivityStateBuilder; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.runtime.ProcessInstanceBuilder; +import org.flowable.identitylink.api.IdentityLink; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.TaskQuery; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.flowable.task.api.history.HistoricTaskInstanceQuery; +import org.flowable.variable.api.history.HistoricVariableInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowApiService") +public class FlowApiServiceImpl implements FlowApiService { + + @Autowired + private RepositoryService repositoryService; + @Autowired + private RuntimeService runtimeService; + @Autowired + private TaskService taskService; + @Autowired + private HistoryService historyService; + @Autowired + private ManagementService managementService; + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowTaskCommentService flowTaskCommentService; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowMessageService flowMessageService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowMultiInstanceTransService flowMultiInstanceTransService; + + @Transactional(rollbackFor = Exception.class) + @Override + public ProcessInstance start(String processDefinitionId, Object dataId) { + TokenData tokenData = TokenData.takeFromRequest(); + Map variableMap = this.initAndGetProcessInstanceVariables(processDefinitionId); + Authentication.setAuthenticatedUserId(tokenData.getLoginName()); + String businessKey = dataId == null ? null : dataId.toString(); + ProcessInstanceBuilder builder = runtimeService.createProcessInstanceBuilder() + .processDefinitionId(processDefinitionId).businessKey(businessKey).variables(variableMap); + if (tokenData.getTenantId() != null) { + builder.tenantId(tokenData.getTenantId().toString()); + } else { + if (tokenData.getAppCode() != null) { + builder.tenantId(tokenData.getAppCode()); + } + } + return builder.start(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Task takeFirstTask(String processInstanceId, FlowTaskComment flowTaskComment, JSONObject taskVariableData) { + String loginName = TokenData.takeFromRequest().getLoginName(); + // 获取流程启动后的第一个任务。 + Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).active().singleResult(); + if (StrUtil.equalsAny(task.getAssignee(), loginName, FlowConstant.START_USER_NAME_VAR)) { + // 按照规则,调用该方法的用户,就是第一个任务的assignee,因此默认会自动执行complete。 + flowTaskComment.fillWith(task); + this.completeTask(task, flowTaskComment, taskVariableData); + } + return task; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public ProcessInstance startAndTakeFirst( + String processDefinitionId, Object dataId, FlowTaskComment flowTaskComment, JSONObject taskVariableData) { + ProcessInstance instance = this.start(processDefinitionId, dataId); + this.takeFirstTask(instance.getProcessInstanceId(), flowTaskComment, taskVariableData); + return instance; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void submitConsign( + HistoricTaskInstance startTaskInstance, Task multiInstanceActiveTask, String newAssignees, boolean isAdd) { + JSONArray assigneeArray = JSON.parseArray(newAssignees); + String multiInstanceExecId = this.getExecutionVariableStringWithSafe( + multiInstanceActiveTask.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + Set assigneeSet = new HashSet<>(StrUtil.split(trans.getAssigneeList(), ",")); + Task runtimeTask = null; + for (int i = 0; i < assigneeArray.size(); i++) { + String assignee = assigneeArray.getString(i); + if (isAdd) { + assigneeSet.add(assignee); + } else { + assigneeSet.remove(assignee); + } + if (isAdd) { + Map variables = new HashMap<>(2); + variables.put("assignee", assigneeArray.getString(i)); + variables.put(FlowConstant.MULTI_SIGN_START_TASK_VAR, startTaskInstance.getId()); + runtimeService.addMultiInstanceExecution( + multiInstanceActiveTask.getTaskDefinitionKey(), multiInstanceActiveTask.getProcessInstanceId(), variables); + } else { + TaskQuery query = taskService.createTaskQuery().active(); + query.processInstanceId(multiInstanceActiveTask.getProcessInstanceId()); + query.taskDefinitionKey(multiInstanceActiveTask.getTaskDefinitionKey()); + query.taskAssignee(assignee); + runtimeTask = query.singleResult(); + if (runtimeTask == null) { + throw new FlowOperationException("审批人 [" + assignee + "] 已经提交审批,不能执行减签操作!"); + } + runtimeService.deleteMultiInstanceExecution(runtimeTask.getExecutionId(), false); + } + } + if (!isAdd && runtimeTask != null) { + this.doChangeTask(runtimeTask); + } + trans.setAssigneeList(StrUtil.join(",", assigneeSet)); + flowMultiInstanceTransService.updateById(trans); + FlowTaskComment flowTaskComment = new FlowTaskComment(); + flowTaskComment.fillWith(startTaskInstance); + flowTaskComment.setApprovalType(isAdd ? FlowApprovalType.MULTI_CONSIGN : FlowApprovalType.MULTI_MINUS_SIGN); + String showName = TokenData.takeFromRequest().getLoginName(); + String comment = String.format("用户 [%s] [%s] [%s]。", isAdd ? "加签" : "减签", showName, newAssignees); + flowTaskComment.setTaskComment(comment); + flowTaskCommentService.saveNew(flowTaskComment); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void completeTask(Task task, FlowTaskComment flowTaskComment, JSONObject taskVariableData) { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + JSONObject passCopyData = (JSONObject) taskVariableData.remove(FlowConstant.COPY_DATA_KEY); + // 判断当前完成执行的任务,是否存在抄送设置。 + Object copyData = runtimeService.getVariable( + task.getProcessInstanceId(), FlowConstant.COPY_DATA_MAP_PREFIX + task.getTaskDefinitionKey()); + if (copyData != null || passCopyData != null) { + JSONObject copyDataJson = this.mergeCopyData(copyData, passCopyData); + flowMessageService.saveNewCopyMessage(task, copyDataJson); + } + if (flowTaskComment != null) { + // 这里处理多实例会签逻辑。 + if (flowTaskComment.getApprovalType().equals(FlowApprovalType.MULTI_SIGN)) { + String loginName = TokenData.takeFromRequest().getLoginName(); + String assigneeList = this.getMultiInstanceAssigneeList(task, taskVariableData); + Assert.isTrue(StrUtil.isNotBlank(assigneeList)); + taskVariableData.put(FlowConstant.MULTI_AGREE_COUNT_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_REFUSE_COUNT_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_ABSTAIN_COUNT_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_SIGN_NUM_OF_INSTANCES_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_SIGN_START_TASK_VAR, task.getId()); + String multiInstanceExecId = MyCommonUtil.generateUuid(); + taskVariableData.put(FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR, multiInstanceExecId); + String comment = String.format("用户 [%s] 会签 [%s]。", loginName, assigneeList); + FlowMultiInstanceTrans multiInstanceTrans = new FlowMultiInstanceTrans(task); + multiInstanceTrans.setMultiInstanceExecId(multiInstanceExecId); + multiInstanceTrans.setAssigneeList(assigneeList); + flowMultiInstanceTransService.saveNew(multiInstanceTrans); + flowTaskComment.setTaskComment(comment); + } + // 处理转办。 + if (FlowApprovalType.TRANSFER.equals(flowTaskComment.getApprovalType())) { + this.transferTo(task, flowTaskComment); + return; + } + this.handleMultiInstanceApprovalType( + task.getExecutionId(), flowTaskComment.getApprovalType(), taskVariableData); + taskVariableData.put(FlowConstant.OPERATION_TYPE_VAR, flowTaskComment.getApprovalType()); + this.setSubmitUserVar(taskVariableData, flowTaskComment); + flowTaskComment.fillWith(task); + if (this.isMultiInstanceTask(task.getProcessDefinitionId(), task.getTaskDefinitionKey())) { + String multiInstanceExecId = getExecutionVariableStringWithSafe( + task.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + FlowMultiInstanceTrans multiInstanceTrans = new FlowMultiInstanceTrans(task); + multiInstanceTrans.setMultiInstanceExecId(multiInstanceExecId); + flowMultiInstanceTransService.saveNew(multiInstanceTrans); + flowTaskComment.setMultiInstanceExecId(multiInstanceExecId); + } + flowTaskCommentService.saveNew(flowTaskComment); + } + taskVariableData.remove(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR); + Integer approvalStatus = MapUtil.getInt(taskVariableData, FlowConstant.LATEST_APPROVAL_STATUS_KEY); + flowWorkOrderService.updateLatestApprovalStatusByProcessInstanceId(task.getProcessInstanceId(), approvalStatus); + taskService.complete(task.getId(), taskVariableData, this.makeTransientVariableMap(taskVariableData)); + flowMessageService.updateFinishedStatusByTaskId(task.getId()); + } + + private void setSubmitUserVar(JSONObject taskVariableData, FlowTaskComment comment) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData != null) { + taskVariableData.put(FlowConstant.SUBMIT_USER_VAR, tokenData.getLoginName()); + } else { + if (StrUtil.isNotBlank(comment.getCreateLoginName())) { + taskVariableData.put(FlowConstant.SUBMIT_USER_VAR, comment.getCreateLoginName()); + } + } + } + + private JSONObject makeTransientVariableMap(JSONObject taskVariableData) { + JSONObject result = new JSONObject(); + if (taskVariableData == null) { + return result; + } + Object masterData = taskVariableData.get(FlowConstant.MASTER_DATA_KEY); + if (masterData != null) { + result.put(FlowConstant.MASTER_DATA_KEY, masterData); + } + Object slaveData = taskVariableData.get(FlowConstant.SLAVE_DATA_KEY); + if (slaveData != null) { + result.put(FlowConstant.SLAVE_DATA_KEY, slaveData); + } + Object masterTable = taskVariableData.get(FlowConstant.MASTER_TABLE_KEY); + if (masterTable != null) { + result.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + } + taskVariableData.remove(FlowConstant.MASTER_DATA_KEY); + taskVariableData.remove(FlowConstant.SLAVE_DATA_KEY); + taskVariableData.remove(FlowConstant.MASTER_TABLE_KEY); + return result; + } + + private String getMultiInstanceAssigneeList(Task task, JSONObject taskVariableData) { + JSONArray assigneeArray = taskVariableData.getJSONArray(FlowConstant.MULTI_ASSIGNEE_LIST_VAR); + String assigneeList; + if (CollUtil.isEmpty(assigneeArray)) { + FlowTaskExt flowTaskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId( + task.getProcessDefinitionId(), task.getTaskDefinitionKey()); + assigneeList = this.buildMutiSignAssigneeList(flowTaskExt.getOperationListJson()); + if (assigneeList != null) { + taskVariableData.put(FlowConstant.MULTI_ASSIGNEE_LIST_VAR, StrUtil.split(assigneeList, ',')); + } + } else { + assigneeList = CollUtil.join(assigneeArray, ","); + } + return assigneeList; + } + + private JSONObject mergeCopyData(Object copyData, JSONObject passCopyData) { + // passCopyData是传阅数据,copyData是抄送数据。 + JSONObject resultCopyDataJson = passCopyData; + if (resultCopyDataJson == null) { + resultCopyDataJson = JSON.parseObject(copyData.toString()); + } else if (copyData != null) { + JSONObject copyDataJson = JSON.parseObject(copyData.toString()); + for (Map.Entry entry : copyDataJson.entrySet()) { + String value = resultCopyDataJson.getString(entry.getKey()); + if (value == null) { + resultCopyDataJson.put(entry.getKey(), entry.getValue()); + } else { + List list1 = StrUtil.split(value, ","); + List list2 = StrUtil.split(entry.getValue().toString(), ","); + Set valueSet = new HashSet<>(list1); + valueSet.addAll(list2); + resultCopyDataJson.put(entry.getKey(), StrUtil.join(",", valueSet)); + } + } + } + this.processMergeCopyData(resultCopyDataJson); + return resultCopyDataJson; + } + + private void processMergeCopyData(JSONObject resultCopyDataJson) { + TokenData tokenData = TokenData.takeFromRequest(); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + for (Map.Entry entry : resultCopyDataJson.entrySet()) { + String type = entry.getKey(); + switch (type) { + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR: + Object upLeaderDeptPostId = + flowIdentityExtHelper.getUpLeaderDeptPostId(tokenData.getDeptId()); + entry.setValue(upLeaderDeptPostId); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR: + Object leaderDeptPostId = + flowIdentityExtHelper.getLeaderDeptPostId(tokenData.getDeptId()); + entry.setValue(leaderDeptPostId); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + Set selfPostIdSet = new HashSet<>(StrUtil.split(entry.getValue().toString(), ",")); + Map deptPostIdMap = + flowIdentityExtHelper.getDeptPostIdMap(tokenData.getDeptId(), selfPostIdSet); + String deptPostIdValues = ""; + if (deptPostIdMap != null) { + deptPostIdValues = StrUtil.join(",", deptPostIdMap.values()); + } + entry.setValue(deptPostIdValues); + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + Set siblingPostIdSet = new HashSet<>(StrUtil.split(entry.getValue().toString(), ",")); + Map siblingDeptPostIdMap = + flowIdentityExtHelper.getSiblingDeptPostIdMap(tokenData.getDeptId(), siblingPostIdSet); + String siblingDeptPostIdValues = ""; + if (siblingDeptPostIdMap != null) { + siblingDeptPostIdValues = StrUtil.join(",", siblingDeptPostIdMap.values()); + } + entry.setValue(siblingDeptPostIdValues); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + Set upPostIdSet = new HashSet<>(StrUtil.split(entry.getValue().toString(), ",")); + Map upDeptPostIdMap = + flowIdentityExtHelper.getUpDeptPostIdMap(tokenData.getDeptId(), upPostIdSet); + String upDeptPostIdValues = ""; + if (upDeptPostIdMap != null) { + upDeptPostIdValues = StrUtil.join(",", upDeptPostIdMap.values()); + } + entry.setValue(upDeptPostIdValues); + break; + default: + break; + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult verifyAssigneeOrCandidateAndClaim(Task task) { + String errorMessage; + String loginName = TokenData.takeFromRequest().getLoginName(); + // 这里必须先执行拾取操作,如果当前用户是候选人,特别是对于分布式场景,更是要先完成候选人的拾取。 + if (task.getAssignee() == null) { + // 没有指派人 + if (!this.isAssigneeOrCandidate(task)) { + errorMessage = "数据验证失败,当前用户不是该待办任务的候选人,请刷新后重试!"; + return CallResult.error(errorMessage); + } + // 作为候选人主动拾取任务。 + taskService.claim(task.getId(), loginName); + } else { + if (!task.getAssignee().equals(loginName)) { + errorMessage = "数据验证失败,当前用户不是该待办任务的指派人,请刷新后重试!"; + return CallResult.error(errorMessage); + } + } + return CallResult.ok(); + } + + @Override + public Map initAndGetProcessInstanceVariables(String processDefinitionId) { + TokenData tokenData = TokenData.takeFromRequest(); + String loginName = tokenData.getLoginName(); + // 设置流程变量。 + Map variableMap = new HashMap<>(4); + variableMap.put(FlowConstant.PROC_INSTANCE_INITIATOR_VAR, loginName); + variableMap.put(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR, loginName); + List flowTaskExtList = flowTaskExtService.getByProcessDefinitionId(processDefinitionId); + boolean hasDeptPostLeader = false; + boolean hasUpDeptPostLeader = false; + boolean hasPostCandidateGroup = false; + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + hasUpDeptPostLeader = true; + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + hasDeptPostLeader = true; + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_POST)) { + hasPostCandidateGroup = true; + } + } + // 如果流程图的配置中包含用户身份相关的变量(如:部门领导和上级领导审批),flowIdentityExtHelper就不能为null。 + // 这个需要子类去实现 BaseFlowIdentityExtHelper 接口,并注册到FlowCustomExtFactory的工厂中。 + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + if (hasUpDeptPostLeader) { + Assert.notNull(flowIdentityExtHelper); + Object upLeaderDeptPostId = flowIdentityExtHelper.getUpLeaderDeptPostId(tokenData.getDeptId()); + if (upLeaderDeptPostId == null) { + variableMap.put(FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, null); + } else { + variableMap.put(FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, upLeaderDeptPostId.toString()); + } + } + if (hasDeptPostLeader) { + Assert.notNull(flowIdentityExtHelper); + Object leaderDeptPostId = flowIdentityExtHelper.getLeaderDeptPostId(tokenData.getDeptId()); + if (leaderDeptPostId == null) { + variableMap.put(FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, null); + } else { + variableMap.put(FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, leaderDeptPostId.toString()); + } + } + if (hasPostCandidateGroup) { + Assert.notNull(flowIdentityExtHelper); + Map postGroupDataMap = + this.buildPostCandidateGroupData(flowIdentityExtHelper, flowTaskExtList); + variableMap.putAll(postGroupDataMap); + } + this.buildCopyData(flowTaskExtList, variableMap); + return variableMap; + } + + private void buildCopyData(List flowTaskExtList, Map variableMap) { + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (StrUtil.isBlank(flowTaskExt.getCopyListJson())) { + continue; + } + List copyDataList = JSON.parseArray(flowTaskExt.getCopyListJson(), JSONObject.class); + Map copyDataMap = new HashMap<>(copyDataList.size()); + for (JSONObject copyData : copyDataList) { + String type = copyData.getString("type"); + String id = copyData.getString("id"); + copyDataMap.put(type, id == null ? "" : id); + } + variableMap.put(FlowConstant.COPY_DATA_MAP_PREFIX + flowTaskExt.getTaskId(), JSON.toJSONString(copyDataMap)); + } + } + + private Map buildPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, List flowTaskExtList) { + Map postVariableMap = MapUtil.newHashMap(); + Set selfPostIdSet = new HashSet<>(); + Set siblingPostIdSet = new HashSet<>(); + Set upPostIdSet = new HashSet<>(); + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (flowTaskExt.getGroupType().equals(FlowConstant.GROUP_TYPE_POST)) { + Assert.notNull(flowTaskExt.getDeptPostListJson()); + List groupDataList = + JSONArray.parseArray(flowTaskExt.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR -> selfPostIdSet.add(groupData.getPostId()); + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR -> siblingPostIdSet.add(groupData.getPostId()); + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR -> upPostIdSet.add(groupData.getPostId()); + default -> { + } + } + } + } + } + postVariableMap.putAll(this.buildSelfPostCandidateGroupData(flowIdentityExtHelper, selfPostIdSet)); + postVariableMap.putAll(this.buildSiblingPostCandidateGroupData(flowIdentityExtHelper, siblingPostIdSet)); + postVariableMap.putAll(this.buildUpPostCandidateGroupData(flowIdentityExtHelper, upPostIdSet)); + return postVariableMap; + } + + private Map buildSelfPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, Set selfPostIdSet) { + Map postVariableMap = MapUtil.newHashMap(); + if (CollUtil.isNotEmpty(selfPostIdSet)) { + Map deptPostIdMap = + flowIdentityExtHelper.getDeptPostIdMap(TokenData.takeFromRequest().getDeptId(), selfPostIdSet); + for (String postId : selfPostIdSet) { + if (MapUtil.isNotEmpty(deptPostIdMap) && deptPostIdMap.containsKey(postId)) { + String deptPostId = deptPostIdMap.get(postId); + postVariableMap.put(FlowConstant.SELF_DEPT_POST_PREFIX + postId, deptPostId); + } else { + postVariableMap.put(FlowConstant.SELF_DEPT_POST_PREFIX + postId, ""); + } + } + } + return postVariableMap; + } + + private Map buildSiblingPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, Set siblingPostIdSet) { + Map postVariableMap = MapUtil.newHashMap(); + if (CollUtil.isNotEmpty(siblingPostIdSet)) { + Map siblingDeptPostIdMap = + flowIdentityExtHelper.getSiblingDeptPostIdMap(TokenData.takeFromRequest().getDeptId(), siblingPostIdSet); + for (String postId : siblingPostIdSet) { + if (MapUtil.isNotEmpty(siblingDeptPostIdMap) && siblingDeptPostIdMap.containsKey(postId)) { + String siblingDeptPostId = siblingDeptPostIdMap.get(postId); + postVariableMap.put(FlowConstant.SIBLING_DEPT_POST_PREFIX + postId, siblingDeptPostId); + } else { + postVariableMap.put(FlowConstant.SIBLING_DEPT_POST_PREFIX + postId, ""); + } + } + } + return postVariableMap; + } + + private Map buildUpPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, Set upPostIdSet) { + Map postVariableMap = MapUtil.newHashMap(); + if (CollUtil.isNotEmpty(upPostIdSet)) { + Map upDeptPostIdMap = + flowIdentityExtHelper.getUpDeptPostIdMap(TokenData.takeFromRequest().getDeptId(), upPostIdSet); + for (String postId : upPostIdSet) { + if (MapUtil.isNotEmpty(upDeptPostIdMap) && upDeptPostIdMap.containsKey(postId)) { + String upDeptPostId = upDeptPostIdMap.get(postId); + postVariableMap.put(FlowConstant.UP_DEPT_POST_PREFIX + postId, upDeptPostId); + } else { + postVariableMap.put(FlowConstant.UP_DEPT_POST_PREFIX + postId, ""); + } + } + } + return postVariableMap; + } + + @Override + public boolean isAssigneeOrCandidate(TaskInfo task) { + String loginName = TokenData.takeFromRequest().getLoginName(); + if (StrUtil.isNotBlank(task.getAssignee())) { + return StrUtil.equals(loginName, task.getAssignee()); + } + TaskQuery query = taskService.createTaskQuery(); + this.buildCandidateCondition(query, loginName); + query.taskId(task.getId()); + return query.active().count() != 0; + } + + @Override + public Collection getProcessAllElements(String processDefinitionId) { + Process process = repositoryService.getBpmnModel(processDefinitionId).getProcesses().get(0); + return this.getAllElements(process.getFlowElements(), null); + } + + @Override + public boolean isProcessInstanceStarter(String processInstanceId) { + String loginName = TokenData.takeFromRequest().getLoginName(); + return historyService.createHistoricProcessInstanceQuery() + .processInstanceId(processInstanceId).startedBy(loginName).count() != 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void setBusinessKeyForProcessInstance(String processInstanceId, Object dataId) { + runtimeService.updateBusinessKey(processInstanceId, dataId.toString()); + } + + @Override + public boolean existActiveProcessInstance(String processInstanceId) { + return runtimeService.createProcessInstanceQuery() + .processInstanceId(processInstanceId).active().count() != 0; + } + + @Override + public ProcessInstance getProcessInstance(String processInstanceId) { + return runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); + } + + @Override + public ProcessInstance getProcessInstanceByBusinessKey(String processDefinitionId, String businessKey) { + return runtimeService.createProcessInstanceQuery() + .processDefinitionId(processDefinitionId).processInstanceBusinessKey(businessKey).singleResult(); + } + + @Override + public Task getProcessInstanceActiveTask(String processInstanceId, String taskId) { + TaskQuery query = taskService.createTaskQuery().processInstanceId(processInstanceId); + if (StrUtil.isNotBlank(taskId)) { + query.taskId(taskId); + } + return query.active().singleResult(); + } + + @Override + public List getProcessInstanceActiveTaskList(String processInstanceId) { + return taskService.createTaskQuery().processInstanceId(processInstanceId).list(); + } + + @Override + public List getProcessInstanceActiveTaskListAndConvert(String processInstanceId) { + List taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list(); + return this.convertToFlowTaskList(taskList); + } + + @Override + public Task getTaskById(String taskId) { + return taskService.createTaskQuery().taskId(taskId).singleResult(); + } + + @Override + public MyPageData getTaskListByUserName( + String username, String definitionKey, String definitionName, String taskName, MyPageParam pageParam) { + TaskQuery query = this.createQuery(); + if (StrUtil.isNotBlank(definitionKey)) { + query.processDefinitionKey(definitionKey); + } + if (StrUtil.isNotBlank(definitionName)) { + query.processDefinitionNameLike("%" + definitionName + "%"); + } + if (StrUtil.isNotBlank(taskName)) { + query.taskNameLike("%" + taskName + "%"); + } + this.buildCandidateCondition(query, username); + long totalCount = query.count(); + query.orderByTaskCreateTime().desc(); + int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + List taskList = query.listPage(firstResult, pageParam.getPageSize()); + return new MyPageData<>(taskList, totalCount); + } + + @Override + public long getTaskCountByUserName(String username) { + TaskQuery query = this.createQuery(); + this.buildCandidateCondition(query, username); + return query.count(); + } + + @Override + public List getTaskListByProcessInstanceIds(List processInstanceIdSet) { + return taskService.createTaskQuery().processInstanceIdIn(processInstanceIdSet).active().list(); + } + + @Override + public List getProcessInstanceList(Set processInstanceIdSet) { + return runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIdSet).list(); + } + + @Override + public ProcessDefinition getProcessDefinitionById(String processDefinitionId) { + return repositoryService.createProcessDefinitionQuery().processDefinitionId(processDefinitionId).singleResult(); + } + + @Override + public List getProcessDefinitionList(Set processDefinitionIdSet) { + return repositoryService.createProcessDefinitionQuery().processDefinitionIds(processDefinitionIdSet).list(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void suspendProcessDefinition(String processDefinitionId) { + repositoryService.suspendProcessDefinitionById(processDefinitionId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void activateProcessDefinition(String processDefinitionId) { + repositoryService.activateProcessDefinitionById(processDefinitionId); + } + + @Override + public BpmnModel getBpmnModelByDefinitionId(String processDefinitionId) { + return repositoryService.getBpmnModel(processDefinitionId); + } + + @Override + public boolean isMultiInstanceTask(String processDefinitionId, String taskKey) { + BpmnModel model = this.getBpmnModelByDefinitionId(processDefinitionId); + FlowElement flowElement = model.getFlowElement(taskKey); + if (!(flowElement instanceof UserTask userTask)) { + return false; + } + return userTask.hasMultiInstanceLoopCharacteristics(); + } + + @Override + public ProcessDefinition getProcessDefinitionByDeployId(String deployId) { + return repositoryService.createProcessDefinitionQuery().deploymentId(deployId).singleResult(); + } + + @Override + public void setProcessInstanceVariables(String processInstanceId, Map variableMap) { + runtimeService.setVariables(processInstanceId, variableMap); + } + + @Override + public Object getProcessInstanceVariable(String processInstanceId, String variableName) { + return runtimeService.getVariable(processInstanceId, variableName); + } + + @Override + public List convertToFlowTaskList(List taskList) { + List flowTaskVoList = new LinkedList<>(); + if (CollUtil.isEmpty(taskList)) { + return flowTaskVoList; + } + Set processDefinitionIdSet = taskList.stream() + .map(Task::getProcessDefinitionId).collect(Collectors.toSet()); + Set procInstanceIdSet = taskList.stream() + .map(Task::getProcessInstanceId).collect(Collectors.toSet()); + List flowEntryPublishList = + flowEntryService.getFlowEntryPublishList(processDefinitionIdSet); + Map flowEntryPublishMap = + flowEntryPublishList.stream().collect(Collectors.toMap(FlowEntryPublish::getProcessDefinitionId, c -> c)); + List instanceList = this.getProcessInstanceList(procInstanceIdSet); + Map instanceMap = + instanceList.stream().collect(Collectors.toMap(ProcessInstance::getId, c -> c)); + List definitionList = this.getProcessDefinitionList(processDefinitionIdSet); + Map definitionMap = + definitionList.stream().collect(Collectors.toMap(ProcessDefinition::getId, c -> c)); + List workOrderList = + flowWorkOrderService.getInList("processInstanceId", procInstanceIdSet); + Map workOrderMap = + workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + for (Task task : taskList) { + FlowTaskVo flowTaskVo = new FlowTaskVo(); + flowTaskVo.setTaskId(task.getId()); + flowTaskVo.setTaskName(task.getName()); + flowTaskVo.setTaskKey(task.getTaskDefinitionKey()); + flowTaskVo.setTaskFormKey(task.getFormKey()); + flowTaskVo.setTaskStartTime(task.getCreateTime()); + flowTaskVo.setEntryId(flowEntryPublishMap.get(task.getProcessDefinitionId()).getEntryId()); + ProcessDefinition processDefinition = definitionMap.get(task.getProcessDefinitionId()); + flowTaskVo.setProcessDefinitionId(processDefinition.getId()); + flowTaskVo.setProcessDefinitionName(processDefinition.getName()); + flowTaskVo.setProcessDefinitionKey(processDefinition.getKey()); + flowTaskVo.setProcessDefinitionVersion(processDefinition.getVersion()); + ProcessInstance processInstance = instanceMap.get(task.getProcessInstanceId()); + flowTaskVo.setProcessInstanceId(processInstance.getId()); + Object initiator = this.getProcessInstanceVariable( + processInstance.getId(), FlowConstant.PROC_INSTANCE_INITIATOR_VAR); + flowTaskVo.setProcessInstanceInitiator(initiator.toString()); + flowTaskVo.setProcessInstanceStartTime(processInstance.getStartTime()); + flowTaskVo.setBusinessKey(processInstance.getBusinessKey()); + FlowWorkOrder flowWorkOrder = workOrderMap.get(task.getProcessInstanceId()); + if (flowWorkOrder != null) { + flowTaskVo.setIsDraft(flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)); + flowTaskVo.setWorkOrderCode(flowWorkOrder.getWorkOrderCode()); + } + flowTaskVoList.add(flowTaskVo); + } + Set loginNameSet = flowTaskVoList.stream() + .map(FlowTaskVo::getProcessInstanceInitiator).collect(Collectors.toSet()); + List flowUserInfos = flowCustomExtFactory + .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); + Map userInfoMap = + flowUserInfos.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + for (FlowTaskVo flowTaskVo : flowTaskVoList) { + FlowUserInfoVo userInfo = userInfoMap.get(flowTaskVo.getProcessInstanceInitiator()); + flowTaskVo.setShowName(userInfo.getShowName()); + flowTaskVo.setHeadImageUrl(userInfo.getHeadImageUrl()); + } + return flowTaskVoList; + } + + @Override + public void addProcessInstanceEndListener(BpmnModel bpmnModel, Class listenerClazz) { + Assert.notNull(listenerClazz); + Process process = bpmnModel.getMainProcess(); + FlowableListener listener = this.createListener("end", listenerClazz.getName()); + process.getExecutionListeners().add(listener); + } + + @Override + public void addExecutionListener( + FlowElement flowElement, + Class listenerClazz, + String event, + List fieldExtensions) { + Assert.notNull(listenerClazz); + FlowableListener listener = this.createListener(event, listenerClazz.getName()); + if (fieldExtensions != null) { + listener.setFieldExtensions(fieldExtensions); + } + flowElement.getExecutionListeners().add(listener); + } + + @Override + public void addTaskCreateListener(UserTask userTask, Class listenerClazz) { + Assert.notNull(listenerClazz); + FlowableListener listener = this.createListener("create", listenerClazz.getName()); + userTask.getTaskListeners().add(listener); + } + + @Override + public HistoricProcessInstance getHistoricProcessInstance(String processInstanceId) { + return historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); + } + + @Override + public List getHistoricProcessInstanceList(Set processInstanceIdSet) { + return historyService.createHistoricProcessInstanceQuery().processInstanceIds(processInstanceIdSet).list(); + } + + @Override + public MyPageData getHistoricProcessInstanceList( + String processDefinitionKey, + String processDefinitionName, + String startUser, + String beginDate, + String endDate, + MyPageParam pageParam, + boolean finishedOnly) throws ParseException { + HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery(); + if (StrUtil.isNotBlank(processDefinitionKey)) { + query.processDefinitionKey(processDefinitionKey); + } + if (StrUtil.isNotBlank(processDefinitionName)) { + query.processDefinitionName(processDefinitionName); + } + if (StrUtil.isNotBlank(startUser)) { + query.startedBy(startUser); + } + if (StrUtil.isNotBlank(beginDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.startedAfter(sdf.parse(beginDate)); + } + if (StrUtil.isNotBlank(endDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.startedBefore(sdf.parse(endDate)); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() != null) { + query.processInstanceTenantId(tokenData.getTenantId().toString()); + } else { + if (tokenData.getAppCode() == null) { + query.processInstanceWithoutTenantId(); + } else { + query.processInstanceTenantId(tokenData.getAppCode()); + } + } + if (finishedOnly) { + query.finished(); + } + query.orderByProcessInstanceStartTime().desc(); + long totalCount = query.count(); + int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + List instanceList = query.listPage(firstResult, pageParam.getPageSize()); + return new MyPageData<>(instanceList, totalCount); + } + + @Override + public MyPageData getHistoricTaskInstanceFinishedList( + String processDefinitionName, + String beginDate, + String endDate, + MyPageParam pageParam) throws ParseException { + String loginName = TokenData.takeFromRequest().getLoginName(); + HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery() + .taskAssignee(loginName) + .finished(); + if (StrUtil.isNotBlank(processDefinitionName)) { + query.processDefinitionName(processDefinitionName); + } + if (StrUtil.isNotBlank(beginDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.taskCompletedAfter(sdf.parse(beginDate)); + } + if (StrUtil.isNotBlank(endDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.taskCompletedBefore(sdf.parse(endDate)); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() != null) { + query.taskTenantId(tokenData.getTenantId().toString()); + } else { + if (StrUtil.isBlank(tokenData.getAppCode())) { + query.taskWithoutTenantId(); + } else { + query.taskTenantId(tokenData.getAppCode()); + } + } + query.orderByHistoricTaskInstanceEndTime().desc(); + long totalCount = query.count(); + int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + List instanceList = query.listPage(firstResult, pageParam.getPageSize()); + return new MyPageData<>(instanceList, totalCount); + } + + @Override + public List getHistoricActivityInstanceList(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list(); + } + + @Override + public List getHistoricActivityInstanceListOrderByStartTime(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list(); + } + + @Override + public HistoricTaskInstance getHistoricTaskInstance(String processInstanceId, String taskId) { + return historyService.createHistoricTaskInstanceQuery() + .processInstanceId(processInstanceId).taskId(taskId).singleResult(); + } + + @Override + public List getHistoricUnfinishedInstanceList(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId).unfinished().list(); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult stopProcessInstance(String processInstanceId, String stopReason, boolean forCancel) { + //需要先更新状态,以便FlowFinishedListener监听器可以正常的判断流程结束的状态。 + int status = FlowTaskStatus.STOPPED; + if (forCancel) { + status = FlowTaskStatus.CANCELLED; + } + return this.stopProcessInstance(processInstanceId, stopReason, status); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult stopProcessInstance(String processInstanceId, String stopReason, int status) { + List taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).active().list(); + if (CollUtil.isEmpty(taskList)) { + return CallResult.error("数据验证失败,当前流程尚未开始或已经结束!"); + } + BpmnModel bpmnModel = repositoryService.getBpmnModel(taskList.get(0).getProcessDefinitionId()); + EndEvent endEvent = bpmnModel.getMainProcess() + .findFlowElementsOfType(EndEvent.class, false).get(0); + List currentActivitiIds = new LinkedList<>(); + flowWorkOrderService.updateFlowStatusByProcessInstanceId(processInstanceId, status); + for (Task task : taskList) { + String currActivityId = task.getTaskDefinitionKey(); + currentActivitiIds.add(currActivityId); + FlowNode currFlow = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currActivityId); + if (currFlow == null) { + List subProcessList = + bpmnModel.getMainProcess().findFlowElementsOfType(SubProcess.class); + for (SubProcess subProcess : subProcessList) { + FlowElement flowElement = subProcess.getFlowElement(currActivityId); + if (flowElement != null) { + currFlow = (FlowNode) flowElement; + break; + } + } + } + org.springframework.util.Assert.notNull(currFlow, "currFlow can't be NULL"); + if (!(currFlow.getParentContainer().equals(endEvent.getParentContainer()))) { + throw new FlowOperationException("数据验证失败,不能从子流程直接中止!"); + } + FlowTaskComment taskComment = new FlowTaskComment(task); + taskComment.setApprovalType(FlowApprovalType.STOP); + taskComment.setTaskComment(stopReason); + flowTaskCommentService.saveNew(taskComment); + } + this.doChangeState(processInstanceId, currentActivitiIds, CollUtil.newArrayList(endEvent.getId())); + flowMessageService.updateFinishedStatusByProcessInstanceId(processInstanceId); + return CallResult.ok(); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteProcessInstance(String processInstanceId) { + historyService.deleteHistoricProcessInstance(processInstanceId); + flowMessageService.removeByProcessInstanceId(processInstanceId); + FlowWorkOrder workOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); + if (workOrder == null) { + return; + } + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(workOrder.getProcessDefinitionKey()); + if (StrUtil.isNotBlank(flowEntry.getExtensionData())) { + FlowEntryExtensionData extData = JSON.parseObject(flowEntry.getExtensionData(), FlowEntryExtensionData.class); + if (BooleanUtil.isTrue(extData.getCascadeDeleteBusinessData())) { + // 级联删除在线表单工作流的业务数据。 + flowCustomExtFactory.getOnlineBusinessDataExtHelper().deleteBusinessData(workOrder); + } + } + flowWorkOrderService.removeByProcessInstanceId(processInstanceId); + } + + @Override + public Object getTaskVariable(String taskId, String variableName) { + return taskService.getVariable(taskId, variableName); + } + + @Override + public String getTaskVariableStringWithSafe(String taskId, String variableName) { + try { + Object v = taskService.getVariable(taskId, variableName); + if (v == null) { + return null; + } + return v.toString(); + } catch (Exception e) { + String errorMessage = + String.format("Failed to getTaskVariable taskId [%s], variableName [%s]", taskId, variableName); + log.error(errorMessage, e); + return null; + } + } + + @Override + public Object getExecutionVariable(String executionId, String variableName) { + return runtimeService.getVariable(executionId, variableName); + } + + @Override + public String getExecutionVariableStringWithSafe(String executionId, String variableName) { + try { + Object v = runtimeService.getVariable(executionId, variableName); + if (v == null) { + return null; + } + return v.toString(); + } catch (Exception e) { + String errorMessage = String.format( + "Failed to getExecutionVariableStringWithSafe executionId [%s], variableName [%s]", executionId, variableName); + log.error(errorMessage, e); + return null; + } + } + + @Override + public Object getHistoricProcessInstanceVariable(String processInstanceId, String variableName) { + HistoricVariableInstance hv = historyService.createHistoricVariableInstanceQuery() + .processInstanceId(processInstanceId).variableName(variableName).singleResult(); + return hv == null ? null : hv.getValue(); + } + + @Override + public BpmnModel convertToBpmnModel(String bpmnXml) throws XMLStreamException { + BpmnXMLConverter converter = new BpmnXMLConverter(); + InputStream in = new ByteArrayInputStream(bpmnXml.getBytes(StandardCharsets.UTF_8)); + @Cleanup XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(in); + return converter.convertToBpmnModel(reader); + } + + @Transactional + @Override + public CallResult backToRuntimeTask(Task task, String targetKey, boolean forReject, String reason) { + String errorMessage; + ProcessDefinition processDefinition = this.getProcessDefinitionById(task.getProcessDefinitionId()); + Collection allElements = this.getProcessAllElements(processDefinition.getId()); + FlowElement source = null; + // 获取跳转的节点元素 + FlowElement target = null; + for (FlowElement flowElement : allElements) { + if (flowElement.getId().equals(task.getTaskDefinitionKey())) { + source = flowElement; + if (StrUtil.isBlank(targetKey)) { + break; + } + } + if (StrUtil.isNotBlank(targetKey)) { + if (flowElement.getId().equals(targetKey)) { + target = flowElement; + } + } + } + if (targetKey != null && target == null) { + errorMessage = "数据验证失败,被驳回的指定目标节点不存在!"; + return CallResult.error(errorMessage); + } + UserTask oneUserTask = null; + List targetIds = null; + if (target == null) { + List parentUserTaskList = this.getParentUserTaskList(source, null, null); + if (CollUtil.isEmpty(parentUserTaskList)) { + errorMessage = "数据验证失败,当前节点为初始任务节点,不能驳回!"; + return CallResult.error(errorMessage); + } + // 获取活动ID, 即节点Key + Set parentUserTaskKeySet = new HashSet<>(); + parentUserTaskList.forEach(item -> parentUserTaskKeySet.add(item.getId())); + List historicActivityIdList = + this.getHistoricActivityInstanceListOrderByStartTime(task.getProcessInstanceId()); + // 数据清洗,将回滚导致的脏数据清洗掉 + List lastHistoricTaskInstanceList = + this.cleanHistoricTaskInstance(allElements, historicActivityIdList); + // 此时历史任务实例为倒序,获取最后走的节点 + targetIds = new ArrayList<>(); + // 循环结束标识,遇到当前目标节点的次数 + int number = 0; + StringBuilder parentHistoricTaskKey = new StringBuilder(); + for (String historicTaskInstanceKey : lastHistoricTaskInstanceList) { + // 当会签时候会出现特殊的,连续都是同一个节点历史数据的情况,这种时候跳过 + if (parentHistoricTaskKey.toString().equals(historicTaskInstanceKey)) { + continue; + } + parentHistoricTaskKey = new StringBuilder(historicTaskInstanceKey); + if (historicTaskInstanceKey.equals(task.getTaskDefinitionKey())) { + number++; + } + if (number == 2) { + break; + } + // 如果当前历史节点,属于父级的节点,说明最后一次经过了这个点,需要退回这个点 + if (parentUserTaskKeySet.contains(historicTaskInstanceKey)) { + targetIds.add(historicTaskInstanceKey); + } + } + // 目的获取所有需要被跳转的节点 currentIds + // 取其中一个父级任务,因为后续要么存在公共网关,要么就是串行公共线路 + oneUserTask = parentUserTaskList.get(0); + } + // 获取所有正常进行的执行任务的活动节点ID,这些任务不能直接使用,需要找出其中需要撤回的任务 + List runExecutionList = + runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId()).list(); + List runActivityIdList = runExecutionList.stream() + .map(Execution::getActivityId) + .filter(StrUtil::isNotBlank).collect(Collectors.toList()); + // 需驳回任务列表 + List currentIds = new ArrayList<>(); + // 通过父级网关的出口连线,结合 runExecutionList 比对,获取需要撤回的任务 + List currentFlowElementList = this.getChildUserTaskList( + target != null ? target : oneUserTask, runActivityIdList, null, null); + currentFlowElementList.forEach(item -> currentIds.add(item.getId())); + if (target == null) { + // 规定:并行网关之前节点必须需存在唯一用户任务节点,如果出现多个任务节点,则并行网关节点默认为结束节点,原因为不考虑多对多情况 + if (targetIds.size() > 1 && currentIds.size() > 1) { + errorMessage = "数据验证失败,任务出现多对多情况,无法撤回!"; + return CallResult.error(errorMessage); + } + } + AtomicReference> tmp = new AtomicReference<>(); + // 用于下面新增网关删除信息时使用 + String targetTmp = targetKey != null ? targetKey : String.join(",", targetIds); + // currentIds 为活动ID列表 + // currentExecutionIds 为执行任务ID列表 + // 需要通过执行任务ID来设置驳回信息,活动ID不行 + currentIds.forEach(currentId -> runExecutionList.forEach(runExecution -> { + if (StrUtil.isNotBlank(runExecution.getActivityId()) && currentId.equals(runExecution.getActivityId())) { + // 查询当前节点的执行任务的历史数据 + tmp.set(historyService.createHistoricActivityInstanceQuery() + .processInstanceId(task.getProcessInstanceId()) + .executionId(runExecution.getId()) + .activityId(runExecution.getActivityId()).list()); + // 如果这个列表的数据只有 1 条数据 + // 网关肯定只有一条,且为包容网关或并行网关 + // 这里的操作目的是为了给网关在扭转前提前加上删除信息,结构与普通节点的删除信息一样,目的是为了知道这个网关也是有经过跳转的 + if (tmp.get() != null && tmp.get().size() == 1 && StrUtil.isNotBlank(tmp.get().get(0).getActivityType()) + && ("parallelGateway".equals(tmp.get().get(0).getActivityType()) || "inclusiveGateway".equals(tmp.get().get(0).getActivityType()))) { + // singleResult 能够执行更新操作 + // 利用 流程实例ID + 执行任务ID + 活动节点ID 来指定唯一数据,保证数据正确 + historyService.createNativeHistoricActivityInstanceQuery().sql( + "UPDATE ACT_HI_ACTINST SET DELETE_REASON_ = 'Change activity to " + targetTmp + "' WHERE PROC_INST_ID_='" + task.getProcessInstanceId() + "' AND EXECUTION_ID_='" + runExecution.getId() + "' AND ACT_ID_='" + runExecution.getActivityId() + "'").singleResult(); + } + } + })); + try { + if (StrUtil.isNotBlank(targetKey)) { + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveActivityIdsToSingleActivityId(currentIds, targetKey).changeState(); + } else { + // 如果父级任务多于 1 个,说明当前节点不是并行节点,原因为不考虑多对多情况 + if (targetIds.size() > 1) { + // 1 对 多任务跳转,currentIds 当前节点(1),targetIds 跳转到的节点(多) + ChangeActivityStateBuilder builder = runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveSingleActivityIdToActivityIds(currentIds.get(0), targetIds); + for (String targetId : targetIds) { + FlowTaskComment taskComment = + flowTaskCommentService.getLatestFlowTaskComment(task.getProcessInstanceId(), targetId); + // 如果驳回后的目标任务包含指定人,则直接通过变量回抄,如果没有则自动忽略该变量,不会给流程带来任何影响。 + String submitLoginName = taskComment.getCreateLoginName(); + if (StrUtil.isNotBlank(submitLoginName)) { + builder.localVariable(targetId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR, submitLoginName); + } + } + builder.changeState(); + } + // 如果父级任务只有一个,因此当前任务可能为网关中的任务 + if (targetIds.size() == 1) { + // 1 对 1 或 多 对 1 情况,currentIds 当前要跳转的节点列表(1或多),targetIds.get(0) 跳转到的节点(1) + // 如果驳回后的目标任务包含指定人,则直接通过变量回抄,如果没有则自动忽略该变量,不会给流程带来任何影响。 + ChangeActivityStateBuilder builder = runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveActivityIdsToSingleActivityId(currentIds, targetIds.get(0)); + FlowTaskComment taskComment = + flowTaskCommentService.getLatestFlowTaskComment(task.getProcessInstanceId(), targetIds.get(0)); + String submitLoginName = taskComment.getCreateLoginName(); + if (StrUtil.isNotBlank(submitLoginName)) { + builder.localVariable(targetIds.get(0), FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR, submitLoginName); + } + builder.changeState(); + } + } + FlowTaskComment comment = new FlowTaskComment(); + comment.setTaskId(task.getId()); + comment.setTaskKey(task.getTaskDefinitionKey()); + comment.setTaskName(task.getName()); + comment.setApprovalType(forReject ? FlowApprovalType.REJECT : FlowApprovalType.REVOKE); + comment.setProcessInstanceId(task.getProcessInstanceId()); + comment.setTaskComment(reason); + flowTaskCommentService.saveNew(comment); + } catch (Exception e) { + log.error("Failed to execute moveSingleActivityIdToActivityIds", e); + return CallResult.error(e.getMessage()); + } + return CallResult.ok(); + } + + private List getParentUserTaskList( + FlowElement source, Set hasSequenceFlow, List userTaskList) { + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = getParentUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList); + } + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 类型为用户节点,则新增父级节点 + if (sequenceFlow.getSourceFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement()); + continue; + } + // 类型为子流程,则添加子流程开始节点出口处相连的节点 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + // 获取子流程用户任务节点 + List childUserTaskList = findChildProcessUserTasks( + (StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && !childUserTaskList.isEmpty()) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 网关场景的继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + userTaskList = getParentUserTaskList( + sequenceFlow.getSourceFlowElement(), new HashSet<>(hasSequenceFlow), userTaskList); + } + } + return userTaskList; + } + + private List getChildUserTaskList( + FlowElement source, List runActiveIdList, Set hasSequenceFlow, List flowElementList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + flowElementList = flowElementList == null ? new ArrayList<>() : flowElementList; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof EndEvent && source.getSubProcess() != null) { + flowElementList = getChildUserTaskList( + source.getSubProcess(), runActiveIdList, hasSequenceFlow, flowElementList); + } + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,或者为网关 + // 活动节点ID 在运行的任务中存在,添加 + FlowElement targetElement = sequenceFlow.getTargetFlowElement(); + if ((targetElement instanceof UserTask || targetElement instanceof Gateway) + && runActiveIdList.contains(targetElement.getId())) { + flowElementList.add(sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = getChildUserTaskList( + (FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), runActiveIdList, hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && !childUserTaskList.isEmpty()) { + flowElementList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + flowElementList = getChildUserTaskList( + sequenceFlow.getTargetFlowElement(), runActiveIdList, new HashSet<>(hasSequenceFlow), flowElementList); + } + } + return flowElementList; + } + + private List cleanHistoricTaskInstance( + Collection allElements, List historicActivityList) { + // 会签节点收集 + List multiTask = new ArrayList<>(); + allElements.forEach(flowElement -> { + if (flowElement instanceof UserTask) { + // 如果该节点的行为为会签行为,说明该节点为会签节点 + if (((UserTask) flowElement).getBehavior() instanceof ParallelMultiInstanceBehavior + || ((UserTask) flowElement).getBehavior() instanceof SequentialMultiInstanceBehavior) { + multiTask.add(flowElement.getId()); + } + } + }); + // 循环放入栈,栈 LIFO:后进先出 + Stack stack = new Stack<>(); + historicActivityList.forEach(stack::push); + // 清洗后的历史任务实例 + List lastHistoricTaskInstanceList = new ArrayList<>(); + // 网关存在可能只走了部分分支情况,且还存在跳转废弃数据以及其他分支数据的干扰,因此需要对历史节点数据进行清洗 + // 临时用户任务 key + StringBuilder userTaskKey = null; + // 临时被删掉的任务 key,存在并行情况 + List deleteKeyList = new ArrayList<>(); + // 临时脏数据线路 + List> dirtyDataLineList = new ArrayList<>(); + // 由某个点跳到会签点,此时出现多个会签实例对应 1 个跳转情况,需要把这些连续脏数据都找到 + // 会签特殊处理下标 + int multiIndex = -1; + // 会签特殊处理 key + StringBuilder multiKey = null; + // 会签特殊处理操作标识 + boolean multiOpera = false; + while (!stack.empty()) { + // 从这里开始 userTaskKey 都还是上个栈的 key + // 是否是脏数据线路上的点 + final boolean[] isDirtyData = {false}; + for (Set oldDirtyDataLine : dirtyDataLineList) { + if (oldDirtyDataLine.contains(stack.peek().getActivityId())) { + isDirtyData[0] = true; + } + } + // 删除原因不为空,说明从这条数据开始回跳或者回退的 + // MI_END:会签完成后,其他未签到节点的删除原因,不在处理范围内 + if (stack.peek().getDeleteReason() != null && !"MI_END".equals(stack.peek().getDeleteReason())) { + // 可以理解为脏线路起点 + String dirtyPoint = ""; + if (stack.peek().getDeleteReason().contains("Change activity to ")) { + dirtyPoint = stack.peek().getDeleteReason().replace("Change activity to ", ""); + } + // 会签回退删除原因有点不同 + if (stack.peek().getDeleteReason().contains("Change parent activity to ")) { + dirtyPoint = stack.peek().getDeleteReason().replace("Change parent activity to ", ""); + } + FlowElement dirtyTask = null; + // 获取变更节点的对应的入口处连线 + // 如果是网关并行回退情况,会变成两条脏数据路线,效果一样 + for (FlowElement flowElement : allElements) { + if (flowElement.getId().equals(stack.peek().getActivityId())) { + dirtyTask = flowElement; + } + } + // 获取脏数据线路 + Set dirtyDataLine = + findDirtyRoads(dirtyTask, null, null, StrUtil.split(dirtyPoint, ','), null); + // 自己本身也是脏线路上的点,加进去 + dirtyDataLine.add(stack.peek().getActivityId()); + log.info(stack.peek().getActivityId() + "点脏路线集合:" + dirtyDataLine); + // 是全新的需要添加的脏线路 + boolean isNewDirtyData = true; + for (Set strings : dirtyDataLineList) { + // 如果发现他的上个节点在脏线路内,说明这个点可能是并行的节点,或者连续驳回 + // 这时,都以之前的脏线路节点为标准,只需合并脏线路即可,也就是路线补全 + if (strings.contains(userTaskKey.toString())) { + isNewDirtyData = false; + strings.addAll(dirtyDataLine); + } + } + // 已确定时全新的脏线路 + if (isNewDirtyData) { + // deleteKey 单一路线驳回到并行,这种同时生成多个新实例记录情况,这时 deleteKey 其实是由多个值组成 + // 按照逻辑,回退后立刻生成的实例记录就是回退的记录 + // 至于驳回所生成的 Key,直接从删除原因中获取,因为存在驳回到并行的情况 + deleteKeyList.add(dirtyPoint + ","); + dirtyDataLineList.add(dirtyDataLine); + } + // 添加后,现在这个点变成脏线路上的点了 + isDirtyData[0] = true; + } + // 如果不是脏线路上的点,说明是有效数据,添加历史实例 Key + if (!isDirtyData[0]) { + lastHistoricTaskInstanceList.add(stack.peek().getActivityId()); + } + // 校验脏线路是否结束 + for (int i = 0; i < deleteKeyList.size(); i ++) { + // 如果发现脏数据属于会签,记录下下标与对应 Key,以备后续比对,会签脏数据范畴开始 + if (multiKey == null && multiTask.contains(stack.peek().getActivityId()) + && deleteKeyList.get(i).contains(stack.peek().getActivityId())) { + multiIndex = i; + multiKey = new StringBuilder(stack.peek().getActivityId()); + } + // 会签脏数据处理,节点退回会签清空 + // 如果在会签脏数据范畴中发现 Key改变,说明会签脏数据在上个节点就结束了,可以把会签脏数据删掉 + if (multiKey != null && !multiKey.toString().equals(stack.peek().getActivityId())) { + deleteKeyList.set(multiIndex , deleteKeyList.get(multiIndex).replace(stack.peek().getActivityId() + ",", "")); + multiKey = null; + // 结束进行下校验删除 + multiOpera = true; + } + // 其他脏数据处理 + // 发现该路线最后一条脏数据,说明这条脏数据线路处理完了,删除脏数据信息 + // 脏数据产生的新实例中是否包含这条数据 + if (multiKey == null && deleteKeyList.get(i).contains(stack.peek().getActivityId())) { + // 删除匹配到的部分 + deleteKeyList.set(i , deleteKeyList.get(i).replace(stack.peek().getActivityId() + ",", "")); + } + // 如果每组中的元素都以匹配过,说明脏数据结束 + if ("".equals(deleteKeyList.get(i))) { + // 同时删除脏数据 + deleteKeyList.remove(i); + dirtyDataLineList.remove(i); + break; + } + } + // 会签数据处理需要在循环外处理,否则可能导致溢出 + // 会签的数据肯定是之前放进去的所以理论上不会溢出,但还是校验下 + if (multiOpera && deleteKeyList.size() > multiIndex && "".equals(deleteKeyList.get(multiIndex))) { + // 同时删除脏数据 + deleteKeyList.remove(multiIndex); + dirtyDataLineList.remove(multiIndex); + multiIndex = -1; + multiOpera = false; + } + // pop() 方法与 peek() 方法不同,在返回值的同时,会把值从栈中移除 + // 保存新的 userTaskKey 在下个循环中使用 + userTaskKey = new StringBuilder(stack.pop().getActivityId()); + } + log.info("清洗后的历史节点数据:" + lastHistoricTaskInstanceList); + return lastHistoricTaskInstanceList; + } + + private List findChildProcessUserTasks(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = findChildProcessUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && !childUserTaskList.isEmpty()) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + userTaskList = findChildProcessUserTasks(sequenceFlow.getTargetFlowElement(), new HashSet<>(hasSequenceFlow), userTaskList); + } + } + return userTaskList; + } + + private Set findDirtyRoads( + FlowElement source, List passRoads, Set hasSequenceFlow, List targets, Set dirtyRoads) { + passRoads = passRoads == null ? new ArrayList<>() : passRoads; + dirtyRoads = dirtyRoads == null ? new HashSet<>() : dirtyRoads; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + dirtyRoads = findDirtyRoads(source.getSubProcess(), passRoads, hasSequenceFlow, targets, dirtyRoads); + } + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 新增经过的路线 + passRoads.add(sequenceFlow.getSourceFlowElement().getId()); + // 如果此点为目标点,确定经过的路线为脏线路,添加点到脏线路中,然后找下个连线 + if (targets.contains(sequenceFlow.getSourceFlowElement().getId())) { + dirtyRoads.addAll(passRoads); + continue; + } + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + dirtyRoads = findChildProcessAllDirtyRoad( + (StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, dirtyRoads); + // 是否存在子流程上,true 是,false 否 + Boolean isInChildProcess = dirtyTargetInChildProcess( + (StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, targets, null); + if (isInChildProcess) { + // 已在子流程上找到,该路线结束 + continue; + } + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + dirtyRoads = findDirtyRoads(sequenceFlow.getSourceFlowElement(), + new ArrayList<>(passRoads), new HashSet<>(hasSequenceFlow), targets, dirtyRoads); + } + } + return dirtyRoads; + } + + private Set findChildProcessAllDirtyRoad( + FlowElement source, Set hasSequenceFlow, Set dirtyRoads) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + dirtyRoads = dirtyRoads == null ? new HashSet<>() : dirtyRoads; + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 添加脏路线 + dirtyRoads.add(sequenceFlow.getTargetFlowElement().getId()); + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + dirtyRoads = findChildProcessAllDirtyRoad( + (FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, dirtyRoads); + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + dirtyRoads = findChildProcessAllDirtyRoad( + sequenceFlow.getTargetFlowElement(), new HashSet<>(hasSequenceFlow), dirtyRoads); + } + } + return dirtyRoads; + } + + private Boolean dirtyTargetInChildProcess( + FlowElement source, Set hasSequenceFlow, List targets, Boolean inChildProcess) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + inChildProcess = inChildProcess != null && inChildProcess; + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null && !inChildProcess) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果发现目标点在子流程上存在,说明只到子流程为止 + if (targets.contains(sequenceFlow.getTargetFlowElement().getId())) { + inChildProcess = true; + break; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + inChildProcess = dirtyTargetInChildProcess((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, targets, inChildProcess); + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + inChildProcess = dirtyTargetInChildProcess(sequenceFlow.getTargetFlowElement(), new HashSet<>(hasSequenceFlow), targets, inChildProcess); + } + } + return inChildProcess; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void transferTo(Task task, FlowTaskComment flowTaskComment) { + List transferUserList = StrUtil.split(flowTaskComment.getDelegateAssignee(), ","); + for (String transferUser : transferUserList) { + if (transferUser.equals(FlowConstant.START_USER_NAME_VAR)) { + String startUser = this.getProcessInstanceVariable( + task.getProcessInstanceId(), FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR).toString(); + String newDelegateAssignee = StrUtil.replace( + flowTaskComment.getDelegateAssignee(), FlowConstant.START_USER_NAME_VAR, startUser); + flowTaskComment.setDelegateAssignee(newDelegateAssignee); + transferUserList = StrUtil.split(flowTaskComment.getDelegateAssignee(), ","); + break; + } + } + taskService.unclaim(task.getId()); + FlowTaskExt taskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId( + task.getProcessDefinitionId(), task.getTaskDefinitionKey()); + if (StrUtil.isNotBlank(taskExt.getCandidateUsernames())) { + List candidateUsernames = this.getCandidateUsernames(taskExt, task.getId()); + if (CollUtil.isNotEmpty(candidateUsernames)) { + for (String username : candidateUsernames) { + taskService.deleteCandidateUser(task.getId(), username); + } + } + } else if (StrUtil.equals(taskExt.getGroupType(), FlowConstant.GROUP_TYPE_ASSIGNEE)) { + List links = taskService.getIdentityLinksForTask(task.getId()); + for (IdentityLink link : links) { + taskService.deleteUserIdentityLink(task.getId(), link.getUserId(), link.getType()); + } + } else { + this.removeCandidateGroup(taskExt, task); + } + transferUserList.forEach(u -> taskService.addCandidateUser(task.getId(), u)); + flowTaskComment.fillWith(task); + flowTaskCommentService.saveNew(flowTaskComment); + } + + @Override + public List getCandidateUsernames(FlowTaskExt flowTaskExt, String taskId) { + if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { + return Collections.emptyList(); + } + if (!StrUtil.equals(flowTaskExt.getCandidateUsernames(), "${" + FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR + "}")) { + return StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); + } + Object candidateUsernames = getTaskVariableStringWithSafe(taskId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); + return candidateUsernames == null ? null : StrUtil.split(candidateUsernames.toString(), ","); + } + + @Override + public Tuple2, Set> getDeptPostIdAndPostIds( + FlowTaskExt flowTaskExt, String processInstanceId, boolean historic) { + Set postIdSet = new LinkedHashSet<>(); + Set deptPostIdSet = new LinkedHashSet<>(); + if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + Object v = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, historic); + if (ObjectUtil.isNotEmpty(v)) { + deptPostIdSet.add(v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + Object v = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, historic); + if (ObjectUtil.isNotEmpty(v)) { + deptPostIdSet.add(v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_POST) + && StrUtil.isNotBlank(flowTaskExt.getDeptPostListJson())) { + this.buildDeptPostIdAndPostIdsForPost(flowTaskExt, processInstanceId, historic, postIdSet, deptPostIdSet); + } + return new Tuple2<>(deptPostIdSet, postIdSet); + } + + @Override + public Map getAllUserTaskMap(String processDefinitionId) { + BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId); + Process process = bpmnModel.getProcesses().get(0); + return process.findFlowElementsOfType(UserTask.class) + .stream().collect(Collectors.toMap(UserTask::getId, a -> a, (k1, k2) -> k1)); + } + + @Override + public UserTask getUserTask(String processDefinitionId, String taskKey) { + BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId); + for (Process process : bpmnModel.getProcesses()) { + UserTask userTask = process.findFlowElementsOfType(UserTask.class) + .stream().filter(t -> t.getId().equals(taskKey)).findFirst().orElse(null); + if (userTask != null) { + return userTask; + } + } + return null; + } + + private void doChangeState(String processInstanceId, List currentIds, List targetIds) { + if (ObjectUtil.hasEmpty(currentIds, targetIds)) { + throw new MyRuntimeException("跳转的源节点和任务节点数量均不能为空!"); + } + ChangeActivityStateBuilder builder = + this.createChangeActivityStateBuilder(currentIds, targetIds, processInstanceId); + targetIds.forEach(targetId -> { + FlowTaskComment comment = flowTaskCommentService.getLatestFlowTaskComment(processInstanceId, targetId); + if (comment != null && StrUtil.isNotBlank(comment.getCreateLoginName())) { + builder.localVariable(targetId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR, comment.getCreateLoginName()); + } + }); + builder.changeState(); + } + + private ChangeActivityStateBuilder createChangeActivityStateBuilder( + List currentIds, List targetIds, String processInstanceId) { + ChangeActivityStateBuilder builder; + if (currentIds.size() > 1 && targetIds.size() > 1) { + builder = new CustomChangeActivityStateBuilderImpl((RuntimeServiceImpl) runtimeService); + ((CustomChangeActivityStateBuilderImpl) builder) + .moveActivityIdsToActivityIds(currentIds, targetIds) + .processInstanceId(processInstanceId); + } else { + builder = runtimeService.createChangeActivityStateBuilder().processInstanceId(processInstanceId); + if (targetIds.size() == 1) { + if (currentIds.size() == 1) { + builder.moveActivityIdTo(currentIds.get(0), targetIds.get(0)); + } else { + builder.moveActivityIdsToSingleActivityId(currentIds, targetIds.get(0)); + } + } else { + builder.moveSingleActivityIdToActivityIds(currentIds.get(0), targetIds); + } + } + return builder; + } + + private void removeCandidateGroup(FlowTaskExt taskExt, Task task) { + if (StrUtil.isNotBlank(taskExt.getDeptIds())) { + for (String deptId : StrUtil.split(taskExt.getDeptIds(), ",")) { + taskService.deleteCandidateGroup(task.getId(), deptId); + } + } + if (StrUtil.isNotBlank(taskExt.getRoleIds())) { + for (String roleId : StrUtil.split(taskExt.getRoleIds(), ",")) { + taskService.deleteCandidateGroup(task.getId(), roleId); + } + } + Tuple2, Set> tuple2 = + getDeptPostIdAndPostIds(taskExt, task.getProcessInstanceId(), false); + if (CollUtil.isNotEmpty(tuple2.getFirst())) { + for (String deptPostId : tuple2.getFirst()) { + taskService.deleteCandidateGroup(task.getId(), deptPostId); + } + } + if (CollUtil.isNotEmpty(tuple2.getSecond())) { + for (String postId : tuple2.getSecond()) { + taskService.deleteCandidateGroup(task.getId(), postId); + } + } + } + + private void buildDeptPostIdAndPostIdsForPost( + FlowTaskExt flowTaskExt, + String processInstanceId, + boolean historic, + Set postIdSet, + Set deptPostIdSet) { + List groupDataList = + JSON.parseArray(flowTaskExt.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR: + postIdSet.add(groupData.getPostId()); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_VAR: + deptPostIdSet.add(groupData.getDeptPostId()); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + Object v = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.SELF_DEPT_POST_PREFIX + groupData.getPostId(), historic); + if (ObjectUtil.isNotEmpty(v)) { + deptPostIdSet.add(v.toString()); + } + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + Object v2 = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.UP_DEPT_POST_PREFIX + groupData.getPostId(), historic); + if (ObjectUtil.isNotEmpty(v2)) { + deptPostIdSet.add(v2.toString()); + } + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + Object v3 = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.SIBLING_DEPT_POST_PREFIX + groupData.getPostId(), historic); + if (ObjectUtil.isNotEmpty(v3)) { + deptPostIdSet.addAll(StrUtil.split(v3.toString(), ",") + .stream().filter(StrUtil::isNotBlank).toList()); + } + break; + default: + break; + } + } + } + + private Object getProcessInstanceVariable(String processInstanceId, String variableName, boolean historic) { + if (historic) { + return getHistoricProcessInstanceVariable(processInstanceId, variableName); + } + return getProcessInstanceVariable(processInstanceId, variableName); + } + + private void handleMultiInstanceApprovalType(String executionId, String approvalType, JSONObject taskVariableData) { + if (StrUtil.isBlank(approvalType)) { + return; + } + if (StrUtil.equalsAny(approvalType, + FlowApprovalType.MULTI_AGREE, + FlowApprovalType.MULTI_REFUSE, + FlowApprovalType.MULTI_ABSTAIN)) { + Map variables = runtimeService.getVariables(executionId); + Integer agreeCount = (Integer) variables.get(FlowConstant.MULTI_AGREE_COUNT_VAR); + Integer refuseCount = (Integer) variables.get(FlowConstant.MULTI_REFUSE_COUNT_VAR); + Integer abstainCount = (Integer) variables.get(FlowConstant.MULTI_ABSTAIN_COUNT_VAR); + Integer nrOfInstances = (Integer) variables.get(FlowConstant.NUMBER_OF_INSTANCES_VAR); + taskVariableData.put(FlowConstant.MULTI_AGREE_COUNT_VAR, agreeCount); + taskVariableData.put(FlowConstant.MULTI_REFUSE_COUNT_VAR, refuseCount); + taskVariableData.put(FlowConstant.MULTI_ABSTAIN_COUNT_VAR, abstainCount); + taskVariableData.put(FlowConstant.MULTI_SIGN_NUM_OF_INSTANCES_VAR, nrOfInstances); + switch (approvalType) { + case FlowApprovalType.MULTI_AGREE: + if (agreeCount == null) { + agreeCount = 0; + } + taskVariableData.put(FlowConstant.MULTI_AGREE_COUNT_VAR, agreeCount + 1); + break; + case FlowApprovalType.MULTI_REFUSE: + if (refuseCount == null) { + refuseCount = 0; + } + taskVariableData.put(FlowConstant.MULTI_REFUSE_COUNT_VAR, refuseCount + 1); + break; + case FlowApprovalType.MULTI_ABSTAIN: + if (abstainCount == null) { + abstainCount = 0; + } + taskVariableData.put(FlowConstant.MULTI_ABSTAIN_COUNT_VAR, abstainCount + 1); + break; + default: + break; + } + } + } + + private TaskQuery createQuery() { + TaskQuery query = taskService.createTaskQuery().active(); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() != null) { + query.taskTenantId(tokenData.getTenantId().toString()); + } else { + if (StrUtil.isBlank(tokenData.getAppCode())) { + query.taskWithoutTenantId(); + } else { + query.taskTenantId(tokenData.getAppCode()); + } + } + return query; + } + + private void buildCandidateCondition(TaskQuery query, String loginName) { + Set groupIdSet = new HashSet<>(); + // NOTE: 需要注意的是,部门Id、部门岗位Id,或者其他类型的分组Id,他们之间一定不能重复。 + TokenData tokenData = TokenData.takeFromRequest(); + Object deptId = tokenData.getDeptId(); + if (deptId != null) { + groupIdSet.add(deptId.toString()); + } + String roleIds = tokenData.getRoleIds(); + if (StrUtil.isNotBlank(tokenData.getRoleIds())) { + groupIdSet.addAll(StrUtil.split(roleIds, ",")); + } + String postIds = tokenData.getPostIds(); + if (StrUtil.isNotBlank(tokenData.getPostIds())) { + groupIdSet.addAll(StrUtil.split(postIds, ",")); + } + String deptPostIds = tokenData.getDeptPostIds(); + if (StrUtil.isNotBlank(deptPostIds)) { + groupIdSet.addAll(StrUtil.split(deptPostIds, ",")); + } + if (CollUtil.isNotEmpty(groupIdSet)) { + query.or().taskCandidateGroupIn(groupIdSet).taskCandidateOrAssigned(loginName).endOr(); + } else { + query.taskCandidateOrAssigned(loginName); + } + } + + private String buildMutiSignAssigneeList(String operationListJson) { + FlowTaskMultiSignAssign multiSignAssignee = null; + List taskOperationList = JSONArray.parseArray(operationListJson, FlowTaskOperation.class); + for (FlowTaskOperation taskOperation : taskOperationList) { + if (FlowApprovalType.MULTI_SIGN.equals(taskOperation.getType())) { + multiSignAssignee = taskOperation.getMultiSignAssignee(); + break; + } + } + org.springframework.util.Assert.notNull(multiSignAssignee, "multiSignAssignee can't be NULL"); + if (UserFilterGroup.USER.equals(multiSignAssignee.getAssigneeType())) { + return multiSignAssignee.getAssigneeList(); + } + Set usernameSet = null; + BaseFlowIdentityExtHelper extHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + Set idSet = CollUtil.newHashSet(StrUtil.split(multiSignAssignee.getAssigneeList(), ",")); + switch (multiSignAssignee.getAssigneeType()) { + case UserFilterGroup.ROLE -> usernameSet = extHelper.getUsernameListByRoleIds(idSet); + case UserFilterGroup.DEPT -> usernameSet = extHelper.getUsernameListByDeptIds(idSet); + case UserFilterGroup.POST -> usernameSet = extHelper.getUsernameListByPostIds(idSet); + case UserFilterGroup.DEPT_POST -> usernameSet = extHelper.getUsernameListByDeptPostIds(idSet); + default -> { + } + } + return CollUtil.isEmpty(usernameSet) ? null : CollUtil.join(usernameSet, ","); + } + + private Collection getAllElements(Collection flowElements, Collection allElements) { + allElements = allElements == null ? new ArrayList<>() : allElements; + for (FlowElement flowElement : flowElements) { + allElements.add(flowElement); + if (flowElement instanceof SubProcess) { + allElements = getAllElements(((SubProcess) flowElement).getFlowElements(), allElements); + } + } + return allElements; + } + + private void doChangeTask(Task runtimeTask) { + Map allUserTaskMap = + this.getAllUserTaskMap(runtimeTask.getProcessDefinitionId()); + UserTask userTaskModel = allUserTaskMap.get(runtimeTask.getTaskDefinitionKey()); + String completeCondition = userTaskModel.getLoopCharacteristics().getCompletionCondition(); + Execution parentExecution = this.getMultiInstanceRootExecution(runtimeTask); + Object nrOfCompletedInstances = runtimeService.getVariable( + parentExecution.getId(), FlowConstant.NUMBER_OF_COMPLETED_INSTANCES_VAR); + Object nrOfInstances = runtimeService.getVariable( + parentExecution.getId(), FlowConstant.NUMBER_OF_INSTANCES_VAR); + ExpressionFactory factory = new ExpressionFactoryImpl(); + SimpleContext context = new SimpleContext(); + context.setVariable("nrOfCompletedInstances", + factory.createValueExpression(nrOfCompletedInstances, Integer.class)); + context.setVariable("nrOfInstances", + factory.createValueExpression(nrOfInstances, Integer.class)); + ValueExpression e = factory.createValueExpression(context, completeCondition, Boolean.class); + Boolean ok = Convert.convert(Boolean.class, e.getValue(context)); + if (BooleanUtil.isTrue(ok)) { + FlowElement targetKey = userTaskModel.getOutgoingFlows().get(0).getTargetFlowElement(); + ChangeActivityStateBuilder builder = runtimeService.createChangeActivityStateBuilder() + .processInstanceId(runtimeTask.getProcessInstanceId()) + .moveActivityIdTo(userTaskModel.getId(), targetKey.getId()); + builder.localVariable(targetKey.getId(), FlowConstant.MULTI_SIGN_NUM_OF_INSTANCES_VAR, nrOfInstances); + builder.changeState(); + } + } + + private Execution getMultiInstanceRootExecution(Task runtimeTask) { + List executionList = runtimeService.createExecutionQuery() + .processInstanceId(runtimeTask.getProcessInstanceId()) + .activityId(runtimeTask.getTaskDefinitionKey()).list(); + for (Execution e : executionList) { + ExecutionEntityImpl ee = (ExecutionEntityImpl) e; + if (ee.isMultiInstanceRoot()) { + return e; + } + } + Execution execution = executionList.get(0); + return runtimeService.createExecutionQuery() + .processInstanceId(runtimeTask.getProcessInstanceId()) + .executionId(execution.getParentId()).singleResult(); + } + + private List getElementIncomingFlows(FlowElement source) { + List sequenceFlows = null; + if (source instanceof org.flowable.bpmn.model.Task) { + sequenceFlows = ((org.flowable.bpmn.model.Task) source).getIncomingFlows(); + } else if (source instanceof Gateway) { + sequenceFlows = ((Gateway) source).getIncomingFlows(); + } else if (source instanceof SubProcess) { + sequenceFlows = ((SubProcess) source).getIncomingFlows(); + } else if (source instanceof StartEvent) { + sequenceFlows = ((StartEvent) source).getIncomingFlows(); + } else if (source instanceof EndEvent) { + sequenceFlows = ((EndEvent) source).getIncomingFlows(); + } + return sequenceFlows; + } + + private List getElementOutgoingFlows(FlowElement source) { + List sequenceFlows = null; + if (source instanceof org.flowable.bpmn.model.Task) { + sequenceFlows = ((org.flowable.bpmn.model.Task) source).getOutgoingFlows(); + } else if (source instanceof Gateway) { + sequenceFlows = ((Gateway) source).getOutgoingFlows(); + } else if (source instanceof SubProcess) { + sequenceFlows = ((SubProcess) source).getOutgoingFlows(); + } else if (source instanceof StartEvent) { + sequenceFlows = ((StartEvent) source).getOutgoingFlows(); + } else if (source instanceof EndEvent) { + sequenceFlows = ((EndEvent) source).getOutgoingFlows(); + } + return sequenceFlows; + } + + private FlowableListener createListener(String eventName, String listenerClassName) { + FlowableListener listener = new FlowableListener(); + listener.setEvent(eventName); + listener.setImplementationType("class"); + listener.setImplementation(listenerClassName); + return listener; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java new file mode 100644 index 00000000..3994aabc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java @@ -0,0 +1,129 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.github.pagehelper.Page; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +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.Date; +import java.util.List; +import java.util.Set; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowCategoryService") +public class FlowCategoryServiceImpl extends BaseService implements FlowCategoryService { + + @Autowired + private FlowCategoryMapper flowCategoryMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowCategoryMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowCategory saveNew(FlowCategory flowCategory) { + flowCategory.setCategoryId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + flowCategory.setAppCode(tokenData.getAppCode()); + flowCategory.setTenantId(tokenData.getTenantId()); + flowCategory.setUpdateUserId(tokenData.getUserId()); + flowCategory.setCreateUserId(tokenData.getUserId()); + Date now = new Date(); + flowCategory.setUpdateTime(now); + flowCategory.setCreateTime(now); + flowCategoryMapper.insert(flowCategory); + return flowCategory; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(FlowCategory flowCategory, FlowCategory originalFlowCategory) { + TokenData tokenData = TokenData.takeFromRequest(); + flowCategory.setAppCode(tokenData.getAppCode()); + flowCategory.setTenantId(tokenData.getTenantId()); + flowCategory.setUpdateUserId(tokenData.getUserId()); + flowCategory.setCreateUserId(originalFlowCategory.getCreateUserId()); + flowCategory.setUpdateTime(new Date()); + flowCategory.setCreateTime(originalFlowCategory.getCreateTime()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return flowCategoryMapper.update(flowCategory, false) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long categoryId) { + return flowCategoryMapper.deleteById(categoryId) == 1; + } + + @Override + public List getFlowCategoryList(FlowCategory filter, String orderBy) { + if (filter == null) { + filter = new FlowCategory(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return flowCategoryMapper.getFlowCategoryList(filter, orderBy); + } + + @Override + public List getFlowCategoryListWithRelation(FlowCategory filter, String orderBy) { + List resultList = this.getFlowCategoryList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public boolean existByCode(String code) { + FlowCategory filter = new FlowCategory(); + filter.setCode(code); + return CollUtil.isNotEmpty(this.getFlowCategoryList(filter, null)); + } + + @Override + public List getInList(Set categoryIds) { + QueryWrapper qw = new QueryWrapper(); + qw.in(FlowCategory::getCategoryId, categoryIds); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getAppCode() == null) { + qw.isNull(FlowCategory::getAppCode); + } else { + qw.eq(FlowCategory::getAppCode, tokenData.getAppCode()); + } + if (tokenData.getTenantId() == null) { + qw.isNull(FlowCategory::getTenantId); + } else { + qw.eq(FlowCategory::getTenantId, tokenData.getTenantId()); + } + return flowCategoryMapper.selectListByQuery(qw); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java new file mode 100644 index 00000000..39183fc8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java @@ -0,0 +1,485 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import com.github.pagehelper.Page; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.flow.listener.*; +import com.orangeforms.common.flow.object.*; +import com.orangeforms.common.flow.util.FlowRedisKeyUtil; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.model.constant.FlowVariableType; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.*; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowEntryService") +public class FlowEntryServiceImpl extends BaseService implements FlowEntryService { + + @Autowired + private FlowEntryMapper flowEntryMapper; + @Autowired + private FlowEntryPublishMapper flowEntryPublishMapper; + @Autowired + private FlowEntryPublishVariableMapper flowEntryPublishVariableMapper; + @Autowired + private FlowEntryVariableService flowEntryVariableService; + @Autowired + private FlowCategoryService flowCategoryService; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private RepositoryService repositoryService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + + private static final Integer FLOW_ENTRY_PUBLISH_TTL = 60 * 60 * 24; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowEntryMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowEntry saveNew(FlowEntry flowEntry) { + flowEntry.setEntryId(idGenerator.nextLongId()); + flowEntry.setStatus(FlowEntryStatus.UNPUBLISHED); + TokenData tokenData = TokenData.takeFromRequest(); + flowEntry.setAppCode(tokenData.getAppCode()); + flowEntry.setTenantId(tokenData.getTenantId()); + flowEntry.setUpdateUserId(tokenData.getUserId()); + flowEntry.setCreateUserId(tokenData.getUserId()); + Date now = new Date(); + flowEntry.setUpdateTime(now); + flowEntry.setCreateTime(now); + flowEntryMapper.insert(flowEntry); + this.insertBuiltinEntryVariables(flowEntry.getEntryId()); + return flowEntry; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void publish(FlowEntry flowEntry, String initTaskInfo) throws XMLStreamException { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + FlowCategory flowCategory = flowCategoryService.getById(flowEntry.getCategoryId()); + InputStream xmlStream = new ByteArrayInputStream( + flowEntry.getBpmnXml().getBytes(StandardCharsets.UTF_8)); + @Cleanup XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(xmlStream); + BpmnXMLConverter converter = new BpmnXMLConverter(); + BpmnModel bpmnModel = converter.convertToBpmnModel(reader); + bpmnModel.getMainProcess().setName(flowEntry.getProcessDefinitionName()); + bpmnModel.getMainProcess().setId(flowEntry.getProcessDefinitionKey()); + flowApiService.addProcessInstanceEndListener(bpmnModel, FlowFinishedListener.class); + List flowTaskExtList = flowTaskExtService.buildTaskExtList(bpmnModel); + if (StrUtil.isNotBlank(flowEntry.getExtensionData())) { + FlowEntryExtensionData flowEntryExtensionData = + JSON.parseObject(flowEntry.getExtensionData(), FlowEntryExtensionData.class); + this.mergeTaskNotifyData(flowEntryExtensionData, flowTaskExtList); + } + this.processFlowTaskExtList(flowTaskExtList, bpmnModel); + TokenData tokenData = TokenData.takeFromRequest(); + Deployment deploy = repositoryService.createDeployment() + .addBpmnModel(flowEntry.getProcessDefinitionKey() + ".bpmn", bpmnModel) + .tenantId(tokenData.getTenantId() != null ? tokenData.getTenantId().toString() : tokenData.getAppCode()) + .name(flowEntry.getProcessDefinitionName()) + .key(flowEntry.getProcessDefinitionKey()) + .category(flowCategory.getCode()) + .deploy(); + ProcessDefinition processDefinition = flowApiService.getProcessDefinitionByDeployId(deploy.getId()); + FlowEntryPublish flowEntryPublish = new FlowEntryPublish(); + flowEntryPublish.setEntryPublishId(idGenerator.nextLongId()); + flowEntryPublish.setEntryId(flowEntry.getEntryId()); + flowEntryPublish.setProcessDefinitionId(processDefinition.getId()); + flowEntryPublish.setDeployId(processDefinition.getDeploymentId()); + flowEntryPublish.setPublishVersion(processDefinition.getVersion()); + flowEntryPublish.setActiveStatus(true); + flowEntryPublish.setMainVersion(flowEntry.getStatus().equals(FlowEntryStatus.UNPUBLISHED)); + flowEntryPublish.setCreateUserId(TokenData.takeFromRequest().getUserId()); + flowEntryPublish.setPublishTime(new Date()); + flowEntryPublish.setInitTaskInfo(initTaskInfo); + flowEntryPublish.setExtensionData(flowEntry.getExtensionData()); + flowEntryPublishMapper.insert(flowEntryPublish); + FlowEntry updatedFlowEntry = new FlowEntry(); + updatedFlowEntry.setEntryId(flowEntry.getEntryId()); + updatedFlowEntry.setStatus(FlowEntryStatus.PUBLISHED); + updatedFlowEntry.setLatestPublishTime(new Date()); + // 对于从未发布过的工作,第一次发布的时候会将本地发布置位主版本。 + if (flowEntry.getStatus().equals(FlowEntryStatus.UNPUBLISHED)) { + updatedFlowEntry.setMainEntryPublishId(flowEntryPublish.getEntryPublishId()); + } + flowEntryMapper.update(updatedFlowEntry); + FlowEntryVariable flowEntryVariableFilter = new FlowEntryVariable(); + flowEntryVariableFilter.setEntryId(flowEntry.getEntryId()); + List flowEntryVariableList = + flowEntryVariableService.getFlowEntryVariableList(flowEntryVariableFilter, null); + if (CollUtil.isNotEmpty(flowTaskExtList)) { + flowTaskExtList.forEach(t -> t.setProcessDefinitionId(processDefinition.getId())); + flowTaskExtService.saveBatch(flowTaskExtList); + } + this.insertEntryPublishVariables(flowEntryVariableList, flowEntryPublish.getEntryPublishId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(FlowEntry flowEntry, FlowEntry originalFlowEntry) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + TokenData tokenData = TokenData.takeFromRequest(); + flowEntry.setAppCode(tokenData.getAppCode()); + flowEntry.setTenantId(tokenData.getTenantId()); + flowEntry.setUpdateUserId(tokenData.getUserId()); + flowEntry.setCreateUserId(originalFlowEntry.getCreateUserId()); + flowEntry.setUpdateTime(new Date()); + flowEntry.setCreateTime(originalFlowEntry.getCreateTime()); + flowEntry.setPageId(originalFlowEntry.getPageId()); + return flowEntryMapper.update(flowEntry) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long entryId) { + FlowEntry flowEntry = this.getById(entryId); + if (flowEntry != null) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + } + if (flowEntryMapper.deleteById(entryId) != 1) { + return false; + } + flowEntryVariableService.removeByEntryId(entryId); + return true; + } + + @Override + public List getFlowEntryList(FlowEntry filter, String orderBy) { + if (filter == null) { + filter = new FlowEntry(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return flowEntryMapper.getFlowEntryList(filter, orderBy); + } + + @Override + public List getFlowEntryListWithRelation(FlowEntry filter, String orderBy) { + List resultList = this.getFlowEntryList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + Set mainEntryPublishIdSet = resultList.stream() + .map(FlowEntry::getMainEntryPublishId).filter(Objects::nonNull).collect(Collectors.toSet()); + if (CollUtil.isNotEmpty(mainEntryPublishIdSet)) { + List mainEntryPublishList = + flowEntryPublishMapper.selectListByIds(mainEntryPublishIdSet); + MyModelUtil.makeOneToOneRelation(FlowEntry.class, resultList, FlowEntry::getMainEntryPublishId, + mainEntryPublishList, FlowEntryPublish::getEntryPublishId, "mainFlowEntryPublish"); + } + return resultList; + } + + @Override + public FlowEntry getFlowEntryFromCache(String processDefinitionKey) { + String key = FlowRedisKeyUtil.makeFlowEntryKey(processDefinitionKey); + QueryWrapper qw = new QueryWrapper(); + qw.eq(FlowEntry::getProcessDefinitionKey, processDefinitionKey); + TokenData tokenData = TokenData.takeFromRequest(); + if (StrUtil.isNotBlank(tokenData.getAppCode())) { + qw.eq(FlowEntry::getAppCode, tokenData.getAppCode()); + } else { + qw.isNull(FlowEntry::getAppCode); + } + if (tokenData.getTenantId() != null) { + qw.eq(FlowEntry::getTenantId, tokenData.getTenantId()); + } else { + qw.isNull(FlowEntry::getTenantId); + } + return commonRedisUtil.getFromCacheWithQueryWrapper(key, qw, flowEntryMapper::selectOneByQuery, FlowEntry.class); + } + + @Override + public List getFlowEntryPublishList(Long entryId) { + FlowEntryPublish filter = new FlowEntryPublish(); + filter.setEntryId(entryId); + QueryWrapper queryWrapper = QueryWrapper.create(filter); + queryWrapper.orderBy(FlowEntryPublish::getEntryPublishId, false); + return flowEntryPublishMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getFlowEntryPublishList(Set processDefinitionIdSet) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(FlowEntryPublish::getProcessDefinitionId, processDefinitionIdSet); + return flowEntryPublishMapper.selectListByQuery(queryWrapper); + } + + @Override + public FlowEntryPublish getFlowEntryPublishFromCache(Long entryPublishId) { + String key = FlowRedisKeyUtil.makeFlowEntryPublishKey(entryPublishId); + return commonRedisUtil.getFromCache( + key, entryPublishId, flowEntryPublishMapper::selectOneById, FlowEntryPublish.class, FLOW_ENTRY_PUBLISH_TTL); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFlowEntryMainVersion(FlowEntry flowEntry, FlowEntryPublish newMainFlowEntryPublish) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(newMainFlowEntryPublish.getEntryPublishId())); + FlowEntryPublish oldMainFlowEntryPublish = + flowEntryPublishMapper.selectOneById(flowEntry.getMainEntryPublishId()); + if (oldMainFlowEntryPublish != null) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(oldMainFlowEntryPublish.getEntryPublishId())); + oldMainFlowEntryPublish.setMainVersion(false); + flowEntryPublishMapper.update(oldMainFlowEntryPublish); + } + newMainFlowEntryPublish.setMainVersion(true); + flowEntryPublishMapper.update(newMainFlowEntryPublish); + FlowEntry updatedEntry = new FlowEntry(); + updatedEntry.setEntryId(flowEntry.getEntryId()); + updatedEntry.setMainEntryPublishId(newMainFlowEntryPublish.getEntryPublishId()); + flowEntryMapper.update(updatedEntry); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void suspendFlowEntryPublish(FlowEntryPublish flowEntryPublish) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(flowEntryPublish.getEntryPublishId())); + FlowEntryPublish updatedEntryPublish = new FlowEntryPublish(); + updatedEntryPublish.setEntryPublishId(flowEntryPublish.getEntryPublishId()); + updatedEntryPublish.setActiveStatus(false); + flowEntryPublishMapper.update(updatedEntryPublish); + flowApiService.suspendProcessDefinition(flowEntryPublish.getProcessDefinitionId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void activateFlowEntryPublish(FlowEntryPublish flowEntryPublish) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(flowEntryPublish.getEntryPublishId())); + FlowEntryPublish updatedEntryPublish = new FlowEntryPublish(); + updatedEntryPublish.setEntryPublishId(flowEntryPublish.getEntryPublishId()); + updatedEntryPublish.setActiveStatus(true); + flowEntryPublishMapper.update(updatedEntryPublish); + flowApiService.activateProcessDefinition(flowEntryPublish.getProcessDefinitionId()); + } + + @Override + public boolean existByProcessDefinitionKey(String processDefinitionKey) { + FlowEntry filter = new FlowEntry(); + filter.setProcessDefinitionKey(processDefinitionKey); + return CollUtil.isNotEmpty(this.getFlowEntryList(filter, null)); + } + + @Override + public CallResult verifyRelatedData(FlowEntry flowEntry, FlowEntry originalFlowEntry) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + if (this.needToVerify(flowEntry, originalFlowEntry, FlowEntry::getCategoryId) + && !flowCategoryService.existId(flowEntry.getCategoryId())) { + return CallResult.error(String.format(errorMessageFormat, "流程类别Id")); + } + return CallResult.ok(); + } + + private void insertBuiltinEntryVariables(Long entryId) { + Date now = new Date(); + FlowEntryVariable operationTypeVariable = new FlowEntryVariable(); + operationTypeVariable.setVariableId(idGenerator.nextLongId()); + operationTypeVariable.setEntryId(entryId); + operationTypeVariable.setVariableName(FlowConstant.OPERATION_TYPE_VAR); + operationTypeVariable.setShowName("审批类型"); + operationTypeVariable.setVariableType(FlowVariableType.TASK); + operationTypeVariable.setBuiltin(true); + operationTypeVariable.setCreateTime(now); + flowEntryVariableService.saveNew(operationTypeVariable); + FlowEntryVariable startUserNameVariable = new FlowEntryVariable(); + startUserNameVariable.setVariableId(idGenerator.nextLongId()); + startUserNameVariable.setEntryId(entryId); + startUserNameVariable.setVariableName("startUserName"); + startUserNameVariable.setShowName("流程启动用户"); + startUserNameVariable.setVariableType(FlowVariableType.INSTANCE); + startUserNameVariable.setBuiltin(true); + startUserNameVariable.setCreateTime(now); + flowEntryVariableService.saveNew(startUserNameVariable); + } + + private void insertEntryPublishVariables(List entryVariableList, Long entryPublishId) { + if (CollUtil.isEmpty(entryVariableList)) { + return; + } + List entryPublishVariableList = + MyModelUtil.copyCollectionTo(entryVariableList, FlowEntryPublishVariable.class); + for (FlowEntryPublishVariable variable : entryPublishVariableList) { + variable.setVariableId(idGenerator.nextLongId()); + variable.setEntryPublishId(entryPublishId); + } + flowEntryPublishVariableMapper.insertList(entryPublishVariableList); + } + + private void mergeTaskNotifyData(FlowEntryExtensionData flowEntryExtensionData, List flowTaskExtList) { + if (CollUtil.isEmpty(flowEntryExtensionData.getNotifyTypes())) { + return; + } + List flowTaskNotifyTypes = + flowEntryExtensionData.getNotifyTypes().stream().filter(StrUtil::isNotBlank).collect(Collectors.toList()); + if (CollUtil.isEmpty(flowTaskNotifyTypes)) { + return; + } + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (flowTaskExt.getExtraDataJson() == null) { + JSONObject o = new JSONObject(); + o.put(FlowConstant.USER_TASK_NOTIFY_TYPES_KEY, flowTaskNotifyTypes); + flowTaskExt.setExtraDataJson(o.toJSONString()); + } else { + FlowUserTaskExtData taskExtData = + JSON.parseObject(flowTaskExt.getExtraDataJson(), FlowUserTaskExtData.class); + if (CollUtil.isEmpty(taskExtData.getFlowNotifyTypeList())) { + taskExtData.setFlowNotifyTypeList(flowTaskNotifyTypes); + } else { + Set notifyTypesSet = taskExtData.getFlowNotifyTypeList() + .stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + notifyTypesSet.addAll(flowTaskNotifyTypes); + taskExtData.setFlowNotifyTypeList(new LinkedList<>(notifyTypesSet)); + } + flowTaskExt.setExtraDataJson(JSON.toJSONString(taskExtData)); + } + } + } + + private void doAddLatestApprovalStatusListener(Collection elementList) { + List sequenceFlowList = + elementList.stream().filter(SequenceFlow.class::isInstance).toList(); + for (FlowElement sequenceFlow : sequenceFlowList) { + FlowElementExtProperty extProperty = flowTaskExtService.buildFlowElementExt(sequenceFlow); + if (extProperty != null && extProperty.getLatestApprovalStatus() != null) { + List fieldExtensions = new LinkedList<>(); + FieldExtension fieldExtension = new FieldExtension(); + fieldExtension.setFieldName(FlowConstant.LATEST_APPROVAL_STATUS_KEY); + fieldExtension.setStringValue(extProperty.getLatestApprovalStatus().toString()); + fieldExtensions.add(fieldExtension); + flowApiService.addExecutionListener( + sequenceFlow, UpdateLatestApprovalStatusListener.class, "start", fieldExtensions); + } + } + List subProcesseList = elementList.stream() + .filter(SubProcess.class::isInstance).map(SubProcess.class::cast).toList(); + for (SubProcess subProcess : subProcesseList) { + this.doAddLatestApprovalStatusListener(subProcess.getFlowElements()); + } + } + + private void calculateAllElementList(Collection elements, List resultList) { + resultList.addAll(elements); + for (FlowElement element : elements) { + if (element instanceof SubProcess) { + this.calculateAllElementList(((SubProcess) element).getFlowElements(), resultList); + } + } + } + + private void processFlowTaskExtList(List flowTaskExtList, BpmnModel bpmnModel) { + List elementList = new LinkedList<>(); + this.calculateAllElementList(bpmnModel.getMainProcess().getFlowElements(), elementList); + this.doAddLatestApprovalStatusListener(elementList); + Map elementMap = elementList.stream() + .filter(UserTask.class::isInstance).collect(Collectors.toMap(FlowElement::getId, c -> c)); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + for (FlowTaskExt t : flowTaskExtList) { + UserTask userTask = (UserTask) elementMap.get(t.getTaskId()); + flowApiService.addTaskCreateListener(userTask, FlowUserTaskListener.class); + Map> attributes = userTask.getAttributes(); + if (CollUtil.isNotEmpty(attributes.get(FlowConstant.USER_TASK_AUTO_SKIP_KEY))) { + flowApiService.addTaskCreateListener(userTask, AutoSkipTaskListener.class); + } + // 如果流程图中包含部门领导审批和上级部门领导审批的选项,就需要注册 FlowCustomExtFactory 工厂中的 + // BaseFlowIdentityExtHelper 对象,该注册操作需要业务模块中实现。 + if (StrUtil.equals(t.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + userTask.setCandidateGroups( + CollUtil.newArrayList("${" + FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR + "}")); + Assert.notNull(flowIdentityExtHelper); + flowApiService.addTaskCreateListener(userTask, flowIdentityExtHelper.getUpDeptPostLeaderListener()); + } else if (StrUtil.equals(t.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + userTask.setCandidateGroups( + CollUtil.newArrayList("${" + FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR + "}")); + Assert.notNull(flowIdentityExtHelper); + flowApiService.addTaskCreateListener(userTask, flowIdentityExtHelper.getDeptPostLeaderListener()); + } else if (StrUtil.equals(t.getGroupType(), FlowConstant.GROUP_TYPE_POST)) { + Assert.notNull(t.getDeptPostListJson()); + List groupDataList = + JSON.parseArray(t.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + List candidateGroupList = + FlowTaskPostCandidateGroup.buildCandidateGroupList(groupDataList); + userTask.setCandidateGroups(candidateGroupList); + } + this.processFlowTaskExtListener(userTask, t); + } + } + + private void processFlowTaskExtListener(UserTask userTask, FlowTaskExt taskExt) { + if (StrUtil.isBlank(taskExt.getExtraDataJson())) { + return; + } + FlowUserTaskExtData userTaskExtData = + JSON.parseObject(taskExt.getExtraDataJson(), FlowUserTaskExtData.class); + if (CollUtil.isNotEmpty(userTaskExtData.getFlowNotifyTypeList())) { + flowApiService.addTaskCreateListener(userTask, FlowTaskNotifyListener.class); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java new file mode 100644 index 00000000..70a60674 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java @@ -0,0 +1,134 @@ +package com.orangeforms.common.flow.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 流程变量数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowEntryVariableService") +public class FlowEntryVariableServiceImpl extends BaseService implements FlowEntryVariableService { + + @Autowired + private FlowEntryVariableMapper flowEntryVariableMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowEntryVariableMapper; + } + + /** + * 保存新增对象。 + * + * @param flowEntryVariable 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowEntryVariable saveNew(FlowEntryVariable flowEntryVariable) { + flowEntryVariable.setVariableId(idGenerator.nextLongId()); + flowEntryVariable.setCreateTime(new Date()); + flowEntryVariableMapper.insert(flowEntryVariable); + return flowEntryVariable; + } + + /** + * 更新数据对象。 + * + * @param flowEntryVariable 更新的对象。 + * @param originalFlowEntryVariable 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(FlowEntryVariable flowEntryVariable, FlowEntryVariable originalFlowEntryVariable) { + flowEntryVariable.setCreateTime(originalFlowEntryVariable.getCreateTime()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return flowEntryVariableMapper.update(flowEntryVariable, false) == 1; + } + + /** + * 删除指定数据。 + * + * @param variableId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long variableId) { + return flowEntryVariableMapper.deleteById(variableId) == 1; + } + + /** + * 删除指定流程Id的所有变量。 + * + * @param entryId 流程Id。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByEntryId(Long entryId) { + flowEntryVariableMapper.deleteByQuery(new QueryWrapper().eq(FlowEntryVariable::getEntryId, entryId)); + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowEntryVariableListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getFlowEntryVariableList(FlowEntryVariable filter, String orderBy) { + return flowEntryVariableMapper.getFlowEntryVariableList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowEntryVariableList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getFlowEntryVariableListWithRelation(FlowEntryVariable filter, String orderBy) { + List resultList = flowEntryVariableMapper.getFlowEntryVariableList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java new file mode 100644 index 00000000..1508bf32 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java @@ -0,0 +1,384 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowMessageOperationType; +import com.orangeforms.common.flow.model.constant.FlowMessageType; +import com.orangeforms.common.flow.dao.FlowMessageIdentityOperationMapper; +import com.orangeforms.common.flow.dao.FlowMessageCandidateIdentityMapper; +import com.orangeforms.common.flow.dao.FlowMessageMapper; +import com.orangeforms.common.flow.object.FlowTaskPostCandidateGroup; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowMessageService; +import com.orangeforms.common.flow.service.FlowTaskExtService; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 工作流消息数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowMessageService") +public class FlowMessageServiceImpl extends BaseService implements FlowMessageService { + + @Autowired + private FlowMessageMapper flowMessageMapper; + @Autowired + private FlowMessageCandidateIdentityMapper flowMessageCandidateIdentityMapper; + @Autowired + private FlowMessageIdentityOperationMapper flowMessageIdentityOperationMapper; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowMessageMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowMessage saveNew(FlowMessage flowMessage) { + flowMessage.setMessageId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData != null) { + flowMessage.setTenantId(tokenData.getTenantId()); + flowMessage.setAppCode(tokenData.getAppCode()); + flowMessage.setCreateUserId(tokenData.getUserId()); + flowMessage.setCreateUsername(tokenData.getShowName()); + flowMessage.setUpdateUserId(tokenData.getUserId()); + } + flowMessage.setCreateTime(new Date()); + flowMessage.setUpdateTime(flowMessage.getCreateTime()); + flowMessageMapper.insert(flowMessage); + return flowMessage; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewRemindMessage(FlowWorkOrder flowWorkOrder) { + List taskList = + flowApiService.getProcessInstanceActiveTaskList(flowWorkOrder.getProcessInstanceId()); + for (Task task : taskList) { + FlowMessage filter = new FlowMessage(); + filter.setTaskId(task.getId()); + List messageList = flowMessageMapper.selectListByQuery(QueryWrapper.create(filter)); + // 同一个任务只能催办一次,多次催办则累加催办次数。 + if (CollUtil.isNotEmpty(messageList)) { + for (FlowMessage flowMessage : messageList) { + flowMessage.setRemindCount(flowMessage.getRemindCount() + 1); + flowMessageMapper.update(flowMessage); + } + continue; + } + FlowMessage flowMessage = BeanUtil.copyProperties(flowWorkOrder, FlowMessage.class); + flowMessage.setMessageType(FlowMessageType.REMIND_TYPE); + flowMessage.setRemindCount(1); + flowMessage.setProcessInstanceInitiator(flowWorkOrder.getSubmitUsername()); + flowMessage.setTaskId(task.getId()); + flowMessage.setTaskName(task.getName()); + flowMessage.setTaskStartTime(task.getCreateTime()); + flowMessage.setTaskAssignee(task.getAssignee()); + flowMessage.setTaskFinished(false); + if (TokenData.takeFromRequest() == null) { + Set usernameSet = CollUtil.newHashSet(flowWorkOrder.getSubmitUsername()); + Map m = flowCustomExtFactory.getFlowIdentityExtHelper().mapUserShowNameByLoginName(usernameSet); + flowMessage.setCreateUsername(m.containsKey(flowWorkOrder.getSubmitUsername()) + ? m.get(flowWorkOrder.getSubmitUsername()) : flowWorkOrder.getSubmitUsername()); + } + this.saveNew(flowMessage); + FlowTaskExt flowTaskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId( + flowWorkOrder.getProcessDefinitionId(), task.getTaskDefinitionKey()); + if (flowTaskExt != null) { + // 插入与当前消息关联任务的候选人 + this.saveMessageCandidateIdentityWithMessage( + flowWorkOrder.getProcessInstanceId(), flowTaskExt, task, flowMessage.getMessageId()); + } + // 插入与当前消息关联任务的指派人。 + if (StrUtil.isNotBlank(task.getAssignee())) { + this.saveMessageCandidateIdentity( + flowMessage.getMessageId(), FlowConstant.GROUP_TYPE_USER_VAR, task.getAssignee()); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewCopyMessage(Task task, JSONObject copyDataJson) { + if (copyDataJson.isEmpty()) { + return; + } + ProcessInstance instance = flowApiService.getProcessInstance(task.getProcessInstanceId()); + FlowMessage flowMessage = new FlowMessage(); + flowMessage.setMessageType(FlowMessageType.COPY_TYPE); + flowMessage.setRemindCount(0); + flowMessage.setProcessDefinitionId(instance.getProcessDefinitionId()); + flowMessage.setProcessDefinitionKey(instance.getProcessDefinitionKey()); + flowMessage.setProcessDefinitionName(instance.getProcessDefinitionName()); + flowMessage.setProcessInstanceId(instance.getProcessInstanceId()); + flowMessage.setProcessInstanceInitiator(instance.getStartUserId()); + flowMessage.setTaskId(task.getId()); + flowMessage.setTaskDefinitionKey(task.getTaskDefinitionKey()); + flowMessage.setTaskName(task.getName()); + flowMessage.setTaskStartTime(task.getCreateTime()); + flowMessage.setTaskAssignee(task.getAssignee()); + flowMessage.setTaskFinished(false); + flowMessage.setOnlineFormData(true); + // 如果是在线表单,这里就保存关联的在线表单Id,便于在线表单业务数据的查找。 + if (BooleanUtil.isTrue(flowMessage.getOnlineFormData())) { + TaskInfoVo taskInfo = JSON.parseObject(task.getFormKey(), TaskInfoVo.class); + flowMessage.setBusinessDataShot(taskInfo.getFormId().toString()); + } + this.saveNew(flowMessage); + for (Map.Entry entry : copyDataJson.entrySet()) { + if (entry.getValue() != null) { + this.saveMessageCandidateIdentityList( + flowMessage.getMessageId(), entry.getKey(), entry.getValue().toString()); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFinishedStatusByTaskId(String taskId) { + FlowMessage flowMessage = new FlowMessage(); + flowMessage.setTaskFinished(true); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowMessage::getTaskId, taskId); + flowMessageMapper.updateByQuery(flowMessage, queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFinishedStatusByProcessInstanceId(String processInstanceId) { + FlowMessage flowMessage = new FlowMessage(); + flowMessage.setTaskFinished(true); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowMessage::getProcessInstanceId, processInstanceId); + flowMessageMapper.updateByQuery(flowMessage, queryWrapper); + } + + @Override + public List getRemindingMessageListByUser() { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.getRemindingMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet()); + } + + @Override + public List getCopyMessageListByUser(Boolean read) { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.getCopyMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet(), read); + } + + @Override + public boolean isCandidateIdentityOnMessage(Long messageId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowMessageCandidateIdentity::getMessageId, messageId); + queryWrapper.in(FlowMessageCandidateIdentity::getCandidateId, buildGroupIdSet()); + return flowMessageCandidateIdentityMapper.selectCountByQuery(queryWrapper) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void readCopyTask(Long messageId) { + FlowMessageIdentityOperation operation = new FlowMessageIdentityOperation(); + operation.setId(idGenerator.nextLongId()); + operation.setMessageId(messageId); + operation.setLoginName(TokenData.takeFromRequest().getLoginName()); + operation.setOperationType(FlowMessageOperationType.READ_FINISHED); + operation.setOperationTime(new Date()); + flowMessageIdentityOperationMapper.insert(operation); + } + + @Override + public int countRemindingMessageListByUser() { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.countRemindingMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet()); + } + + @Override + public int countCopyMessageByUser() { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.countCopyMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByProcessInstanceId(String processInstanceId) { + flowMessageCandidateIdentityMapper.deleteByProcessInstanceId(processInstanceId); + flowMessageIdentityOperationMapper.deleteByProcessInstanceId(processInstanceId); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowMessage::getProcessInstanceId, processInstanceId); + flowMessageMapper.deleteByQuery(queryWrapper); + } + + private Set buildGroupIdSet() { + TokenData tokenData = TokenData.takeFromRequest(); + Set groupIdSet = new HashSet<>(1); + groupIdSet.add(tokenData.getLoginName()); + this.parseAndAddIdArray(groupIdSet, tokenData.getRoleIds()); + this.parseAndAddIdArray(groupIdSet, tokenData.getDeptPostIds()); + this.parseAndAddIdArray(groupIdSet, tokenData.getPostIds()); + if (tokenData.getDeptId() != null) { + groupIdSet.add(tokenData.getDeptId().toString()); + } + return groupIdSet; + } + + private void parseAndAddIdArray(Set groupIdSet, String idArray) { + if (StrUtil.isNotBlank(idArray)) { + if (groupIdSet == null) { + groupIdSet = new HashSet<>(); + } + groupIdSet.addAll(StrUtil.split(idArray, ',')); + } + } + + private void saveMessageCandidateIdentityWithMessage( + String processInstanceId, FlowTaskExt flowTaskExt, Task task, Long messageId) { + List candidates = flowApiService.getCandidateUsernames(flowTaskExt, task.getId()); + if (CollUtil.isNotEmpty(candidates)) { + this.saveMessageCandidateIdentityList( + messageId, FlowConstant.GROUP_TYPE_USER_VAR, CollUtil.join(candidates, ",")); + } + this.saveMessageCandidateIdentityList( + messageId, FlowConstant.GROUP_TYPE_ROLE_VAR, flowTaskExt.getRoleIds()); + this.saveMessageCandidateIdentityList( + messageId, FlowConstant.GROUP_TYPE_DEPT_VAR, flowTaskExt.getDeptIds()); + if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + Object v = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR); + if (v != null) { + this.saveMessageCandidateIdentity( + messageId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + Object v = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR); + if (v != null) { + this.saveMessageCandidateIdentity( + messageId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_POST)) { + Assert.notBlank(flowTaskExt.getDeptPostListJson()); + List groupDataList = + JSONArray.parseArray(flowTaskExt.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + this.saveMessageCandidateIdentity(messageId, processInstanceId, groupData); + } + } + } + + private void saveMessageCandidateIdentity( + Long messageId, String processInstanceId, FlowTaskPostCandidateGroup groupData) { + FlowMessageCandidateIdentity candidateIdentity = new FlowMessageCandidateIdentity(); + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setMessageId(messageId); + candidateIdentity.setCandidateType(groupData.getType()); + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR: + candidateIdentity.setCandidateId(groupData.getPostId()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_VAR: + candidateIdentity.setCandidateId(groupData.getDeptPostId()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + Object v = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.SELF_DEPT_POST_PREFIX + groupData.getPostId()); + if (v != null) { + candidateIdentity.setCandidateId(v.toString()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + Object v2 = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.UP_DEPT_POST_PREFIX + groupData.getPostId()); + if (v2 != null) { + candidateIdentity.setCandidateId(v2.toString()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + Object v3 = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.SIBLING_DEPT_POST_PREFIX + groupData.getPostId()); + if (v3 != null) { + List candidateIds = StrUtil.split(v3.toString(), ","); + for (String candidateId : candidateIds) { + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setCandidateId(candidateId); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + } + break; + default: + break; + } + } + private void saveMessageCandidateIdentity(Long messageId, String candidateType, String candidateId) { + FlowMessageCandidateIdentity candidateIdentity = new FlowMessageCandidateIdentity(); + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setMessageId(messageId); + candidateIdentity.setCandidateType(candidateType); + candidateIdentity.setCandidateId(candidateId); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + + private void saveMessageCandidateIdentityList(Long messageId, String candidateType, String identityIds) { + if (StrUtil.isNotBlank(identityIds)) { + for (String identityId : StrUtil.split(identityIds, ',')) { + FlowMessageCandidateIdentity candidateIdentity = new FlowMessageCandidateIdentity(); + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setMessageId(messageId); + candidateIdentity.setCandidateType(candidateType); + candidateIdentity.setCandidateId(identityId); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java new file mode 100644 index 00000000..a94192a3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.flow.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.dao.FlowMultiInstanceTransMapper; +import com.orangeforms.common.flow.model.FlowMultiInstanceTrans; +import com.orangeforms.common.flow.service.FlowMultiInstanceTransService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.Date; + +/** + * 会签任务操作流水数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowMultiInstanceTransService") +public class FlowMultiInstanceTransServiceImpl + extends BaseService implements FlowMultiInstanceTransService { + + @Autowired + private FlowMultiInstanceTransMapper flowMultiInstanceTransMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowMultiInstanceTransMapper; + } + + /** + * 保存新增对象。 + * + * @param flowMultiInstanceTrans 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowMultiInstanceTrans saveNew(FlowMultiInstanceTrans flowMultiInstanceTrans) { + flowMultiInstanceTrans.setId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + flowMultiInstanceTrans.setCreateUserId(tokenData.getUserId()); + flowMultiInstanceTrans.setCreateLoginName(tokenData.getLoginName()); + flowMultiInstanceTrans.setCreateUsername(tokenData.getShowName()); + flowMultiInstanceTrans.setCreateTime(new Date()); + flowMultiInstanceTransMapper.insert(flowMultiInstanceTrans); + return flowMultiInstanceTrans; + } + + @Override + public FlowMultiInstanceTrans getByExecutionId(String executionId, String taskId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowMultiInstanceTrans::getExecutionId, executionId); + queryWrapper.eq(FlowMultiInstanceTrans::getTaskId, taskId); + return flowMultiInstanceTransMapper.selectOneByQuery(queryWrapper); + } + + @Override + public FlowMultiInstanceTrans getWithAssigneeListByMultiInstanceExecId(String multiInstanceExecId) { + if (multiInstanceExecId == null) { + return null; + } + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowMultiInstanceTrans::getMultiInstanceExecId, multiInstanceExecId); + queryWrapper.isNotNull(FlowMultiInstanceTrans::getAssigneeList); + return flowMultiInstanceTransMapper.selectOneByQuery(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java new file mode 100644 index 00000000..41b1e3f3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java @@ -0,0 +1,140 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 流程任务批注数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowTaskCommentService") +public class FlowTaskCommentServiceImpl extends BaseService implements FlowTaskCommentService { + + @Autowired + private FlowTaskCommentMapper flowTaskCommentMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowTaskCommentMapper; + } + + /** + * 保存新增对象。 + * + * @param flowTaskComment 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowTaskComment saveNew(FlowTaskComment flowTaskComment) { + flowTaskComment.setId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData != null) { + flowTaskComment.setHeadImageUrl(tokenData.getHeadImageUrl()); + flowTaskComment.setCreateUserId(tokenData.getUserId()); + flowTaskComment.setCreateLoginName(tokenData.getLoginName()); + flowTaskComment.setCreateUsername(tokenData.getShowName()); + } + flowTaskComment.setCreateTime(new Date()); + flowTaskCommentMapper.insert(flowTaskComment); + FlowTaskComment.setToRequest(flowTaskComment); + return flowTaskComment; + } + + /** + * 查询指定流程实例Id下的所有审批任务的批注。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果集。 + */ + @Override + public List getFlowTaskCommentList(String processInstanceId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.orderBy(FlowTaskComment::getId, true); + return flowTaskCommentMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getFlowTaskCommentListByTaskIds(Set taskIdSet) { + QueryWrapper queryWrapper = new QueryWrapper().in(FlowTaskComment::getTaskId, taskIdSet); + queryWrapper.orderBy(FlowTaskComment::getId, false); + return flowTaskCommentMapper.selectListByQuery(queryWrapper); + } + + @Override + public FlowTaskComment getLatestFlowTaskComment(String processInstanceId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.orderBy(FlowTaskComment::getId, false); + Page pageData = flowTaskCommentMapper.paginate(new Page<>(1, 1), queryWrapper); + return CollUtil.isEmpty(pageData.getRecords()) ? null : pageData.getRecords().get(0); + } + + @Override + public FlowTaskComment getLatestFlowTaskComment(String processInstanceId, String taskDefinitionKey) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.eq(FlowTaskComment::getTaskKey, taskDefinitionKey); + queryWrapper.orderBy(FlowTaskComment::getId, false); + Page pageData = flowTaskCommentMapper.paginate(new Page<>(1, 1), queryWrapper); + return CollUtil.isEmpty(pageData.getRecords()) ? null : pageData.getRecords().get(0); + } + + @Override + public FlowTaskComment getFirstFlowTaskComment(String processInstanceId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.orderBy(FlowTaskComment::getId, true); + Page pageData = flowTaskCommentMapper.paginate(new Page<>(1, 1), queryWrapper); + return CollUtil.isEmpty(pageData.getRecords()) ? null : pageData.getRecords().get(0); + } + + @Override + public List getFlowTaskCommentListByExecutionId( + String processInstanceId, String taskId, String executionId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.eq(FlowTaskComment::getTaskId, taskId); + queryWrapper.eq(FlowTaskComment::getExecutionId, executionId); + queryWrapper.orderBy(FlowTaskComment::getCreateTime, true); + return flowTaskCommentMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getFlowTaskCommentListByMultiInstanceExecId(String multiInstanceExecId) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowTaskComment::getMultiInstanceExecId, multiInstanceExecId); + return flowTaskCommentMapper.selectListByQuery(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java new file mode 100644 index 00000000..ad2a5a83 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java @@ -0,0 +1,622 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.object.FlowElementExtProperty; +import com.orangeforms.common.flow.object.FlowTaskMultiSignAssign; +import com.orangeforms.common.flow.object.FlowUserTaskExtData; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; +import org.flowable.task.api.TaskInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowTaskExtService") +public class FlowTaskExtServiceImpl extends BaseService implements FlowTaskExtService { + + @Autowired + private FlowTaskExtMapper flowTaskExtMapper; + @Autowired + private FlowEntryVariableService flowEntryVariableService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowMultiInstanceTransService flowMultiInstanceTransService; + @Autowired + private FlowTaskCommentService flowTaskCommentService; + + private static final String ID = "id"; + private static final String TYPE = "type"; + private static final String LABEL = "label"; + private static final String NAME = "name"; + private static final String VALUE = "value"; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowTaskExtMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveBatch(List flowTaskExtList) { + if (CollUtil.isNotEmpty(flowTaskExtList)) { + flowTaskExtMapper.insertList(flowTaskExtList); + } + } + + @Override + public FlowTaskExt getByProcessDefinitionIdAndTaskId(String processDefinitionId, String taskId) { + FlowTaskExt filter = new FlowTaskExt(); + filter.setProcessDefinitionId(processDefinitionId); + filter.setTaskId(taskId); + return flowTaskExtMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + @Override + public List getByProcessDefinitionId(String processDefinitionId) { + FlowTaskExt filter = new FlowTaskExt(); + filter.setProcessDefinitionId(processDefinitionId); + return flowTaskExtMapper.selectListByQuery(QueryWrapper.create(filter)); + } + + @Override + public List getCandidateUserInfoList( + String processInstanceId, + FlowTaskExt flowTaskExt, + TaskInfo taskInfo, + boolean isMultiInstanceTask, + boolean historic) { + List resultUserMapList = new LinkedList<>(); + if (!isMultiInstanceTask && this.buildTransferUserList(taskInfo, resultUserMapList)) { + return resultUserMapList; + } + Set loginNameSet = new HashSet<>(); + this.buildFlowUserInfoListByDeptAndRoleIds(flowTaskExt, loginNameSet, resultUserMapList); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + Set usernameSet = new HashSet<>(); + switch (flowTaskExt.getGroupType()) { + case FlowConstant.GROUP_TYPE_ASSIGNEE: + usernameSet.add(taskInfo.getAssignee()); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_LEADER: + String deptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + taskInfo.getExecutionId(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR); + List userInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(deptPostLeaderId)); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER: + String upDeptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + taskInfo.getExecutionId(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR); + List upUserInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(upDeptPostLeaderId)); + this.buildUserMapList(upUserInfoList, loginNameSet, resultUserMapList); + break; + default: + break; + } + List candidateUsernames = flowApiService.getCandidateUsernames(flowTaskExt, taskInfo.getId()); + if (CollUtil.isNotEmpty(candidateUsernames)) { + usernameSet.addAll(candidateUsernames); + } + if (isMultiInstanceTask) { + List assigneeList = this.getAssigneeList(taskInfo.getExecutionId(), taskInfo.getId()); + if (CollUtil.isNotEmpty(assigneeList)) { + usernameSet.addAll(assigneeList); + } + } + if (CollUtil.isNotEmpty(usernameSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByUsernameSet(usernameSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + Tuple2, Set> tuple2 = + flowApiService.getDeptPostIdAndPostIds(flowTaskExt, processInstanceId, historic); + Set postIdSet = tuple2.getSecond(); + Set deptPostIdSet = tuple2.getFirst(); + if (CollUtil.isNotEmpty(postIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByPostIds(postIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + if (CollUtil.isNotEmpty(deptPostIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByDeptPostIds(deptPostIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + return resultUserMapList; + } + + @Override + public List getCandidateUserInfoList( + String processInstanceId, + String executionId, + FlowTaskExt flowTaskExt) { + List resultUserMapList = new LinkedList<>(); + Set loginNameSet = new HashSet<>(); + this.buildFlowUserInfoListByDeptAndRoleIds(flowTaskExt, loginNameSet, resultUserMapList); + Set usernameSet = new HashSet<>(); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + switch (flowTaskExt.getGroupType()) { + case FlowConstant.GROUP_TYPE_DEPT_POST_LEADER: + String deptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + executionId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR); + List userInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(deptPostLeaderId)); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER: + String upDeptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + executionId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR); + List upUserInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(upDeptPostLeaderId)); + this.buildUserMapList(upUserInfoList, loginNameSet, resultUserMapList); + break; + default: + break; + } + List candidateUsernames; + if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { + candidateUsernames = Collections.emptyList(); + } else { + if (!StrUtil.equals(flowTaskExt.getCandidateUsernames(), "${" + FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR + "}")) { + candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); + } else { + Object v = flowApiService.getExecutionVariableStringWithSafe(executionId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); + candidateUsernames = v == null ? null : StrUtil.split(v.toString(), ","); + } + } + if (CollUtil.isNotEmpty(candidateUsernames)) { + usernameSet.addAll(candidateUsernames); + } + if (CollUtil.isNotEmpty(usernameSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByUsernameSet(usernameSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + Tuple2, Set> tuple2 = + flowApiService.getDeptPostIdAndPostIds(flowTaskExt, processInstanceId, false); + Set postIdSet = tuple2.getSecond(); + Set deptPostIdSet = tuple2.getFirst(); + if (CollUtil.isNotEmpty(postIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByPostIds(postIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + if (CollUtil.isNotEmpty(deptPostIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByDeptPostIds(deptPostIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + return resultUserMapList; + } + + private void buildUserMapList( + List userInfoList, Set loginNameSet, List userMapList) { + if (CollUtil.isEmpty(userInfoList)) { + return; + } + for (FlowUserInfoVo userInfo : userInfoList) { + if (!loginNameSet.contains(userInfo.getLoginName())) { + loginNameSet.add(userInfo.getLoginName()); + userMapList.add(userInfo); + } + } + } + + @Override + public FlowTaskExt buildTaskExtByUserTask(UserTask userTask) { + FlowTaskExt flowTaskExt = new FlowTaskExt(); + flowTaskExt.setTaskId(userTask.getId()); + String formKey = userTask.getFormKey(); + if (StrUtil.isNotBlank(formKey)) { + TaskInfoVo taskInfoVo = JSON.parseObject(formKey, TaskInfoVo.class); + flowTaskExt.setGroupType(taskInfoVo.getGroupType()); + } + JSONObject extraDataJson = this.buildFlowTaskExtensionData(userTask); + if (extraDataJson != null) { + flowTaskExt.setExtraDataJson(extraDataJson.toJSONString()); + } + Map> extensionMap = userTask.getExtensionElements(); + if (MapUtil.isEmpty(extensionMap)) { + return flowTaskExt; + } + List operationList = this.buildOperationListExtensionElement(extensionMap); + if (CollUtil.isNotEmpty(operationList)) { + flowTaskExt.setOperationListJson(JSON.toJSONString(operationList)); + } + List variableList = this.buildVariableListExtensionElement(extensionMap); + if (CollUtil.isNotEmpty(variableList)) { + flowTaskExt.setVariableListJson(JSON.toJSONString(variableList)); + } + JSONObject assigneeListObject = this.buildAssigneeListExtensionElement(extensionMap); + if (assigneeListObject != null) { + flowTaskExt.setAssigneeListJson(JSON.toJSONString(assigneeListObject)); + } + List deptPostList = this.buildDeptPostListExtensionElement(extensionMap); + if (deptPostList != null) { + flowTaskExt.setDeptPostListJson(JSON.toJSONString(deptPostList)); + } + List copyList = this.buildCopyListExtensionElement(extensionMap); + if (copyList != null) { + flowTaskExt.setCopyListJson(JSON.toJSONString(copyList)); + } + JSONObject candidateGroupObject = this.buildUserCandidateGroupsExtensionElement(extensionMap); + if (candidateGroupObject != null) { + String type = candidateGroupObject.getString(TYPE); + String value = candidateGroupObject.getString(VALUE); + switch (type) { + case "DEPT": + flowTaskExt.setDeptIds(value); + break; + case "ROLE": + flowTaskExt.setRoleIds(value); + break; + case "USERS": + flowTaskExt.setCandidateUsernames(value); + break; + default: + break; + } + } + return flowTaskExt; + } + + @Override + public List buildTaskExtList(BpmnModel bpmnModel) { + List processList = bpmnModel.getProcesses(); + List flowTaskExtList = new LinkedList<>(); + for (Process process : processList) { + for (FlowElement element : process.getFlowElements()) { + this.doBuildTaskExtList(element, flowTaskExtList); + } + } + return flowTaskExtList; + } + + @Override + public List buildOperationListExtensionElement(Map> extensionMap) { + List formOperationElements = + this.getMyExtensionElementList(extensionMap, "operationList", "formOperation"); + if (CollUtil.isEmpty(formOperationElements)) { + return Collections.emptyList(); + } + List resultList = new LinkedList<>(); + for (ExtensionElement e : formOperationElements) { + JSONObject operationJsonData = new JSONObject(); + operationJsonData.put(ID, e.getAttributeValue(null, ID)); + operationJsonData.put(LABEL, e.getAttributeValue(null, LABEL)); + operationJsonData.put(TYPE, e.getAttributeValue(null, TYPE)); + operationJsonData.put("showOrder", e.getAttributeValue(null, "showOrder")); + operationJsonData.put("latestApprovalStatus", e.getAttributeValue(null, "latestApprovalStatus")); + String multiSignAssignee = e.getAttributeValue(null, "multiSignAssignee"); + if (StrUtil.isNotBlank(multiSignAssignee)) { + operationJsonData.put("multiSignAssignee", + JSON.parseObject(multiSignAssignee, FlowTaskMultiSignAssign.class)); + } + resultList.add(operationJsonData); + } + return resultList; + } + + @Override + public List buildVariableListExtensionElement(Map> extensionMap) { + List formVariableElements = + this.getMyExtensionElementList(extensionMap, "variableList", "formVariable"); + if (CollUtil.isEmpty(formVariableElements)) { + return Collections.emptyList(); + } + Set variableIdSet = new HashSet<>(); + for (ExtensionElement e : formVariableElements) { + String id = e.getAttributeValue(null, ID); + variableIdSet.add(Long.parseLong(id)); + } + List variableList = flowEntryVariableService.getInList(variableIdSet); + List resultList = new LinkedList<>(); + for (FlowEntryVariable variable : variableList) { + resultList.add((JSONObject) JSON.toJSON(variable)); + } + return resultList; + } + + @Override + public FlowElementExtProperty buildFlowElementExt(FlowElement element) { + JSONObject propertiesData = this.buildFlowElementExtToJson(element); + return propertiesData == null ? null : propertiesData.toJavaObject(FlowElementExtProperty.class); + } + + @Override + public JSONObject buildFlowElementExtToJson(FlowElement element) { + Map> extensionMap = element.getExtensionElements(); + List propertiesElements = + this.getMyExtensionElementList(extensionMap, "properties", "property"); + if (CollUtil.isEmpty(propertiesElements)) { + return null; + } + JSONObject propertiesData = new JSONObject(); + for (ExtensionElement e : propertiesElements) { + String name = e.getAttributeValue(null, NAME); + String value = e.getAttributeValue(null, VALUE); + propertiesData.put(name, value); + } + return propertiesData; + } + + private void doBuildTaskExtList(FlowElement element, List flowTaskExtList) { + if (element instanceof UserTask) { + FlowTaskExt flowTaskExt = this.buildTaskExtByUserTask((UserTask) element); + flowTaskExtList.add(flowTaskExt); + } else if (element instanceof SubProcess) { + Collection flowElements = ((SubProcess) element).getFlowElements(); + for (FlowElement element1 : flowElements) { + this.doBuildTaskExtList(element1, flowTaskExtList); + } + } + } + + private void buildFlowUserInfoListByDeptAndRoleIds( + FlowTaskExt flowTaskExt, Set loginNameSet, List resultUserMapList) { + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + if (StrUtil.isNotBlank(flowTaskExt.getDeptIds())) { + Set deptIdSet = CollUtil.newHashSet(StrUtil.split(flowTaskExt.getDeptIds(), ',')); + List userInfoList = flowIdentityExtHelper.getUserInfoListByDeptIds(deptIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + if (StrUtil.isNotBlank(flowTaskExt.getRoleIds())) { + Set roleIdSet = CollUtil.newHashSet(StrUtil.split(flowTaskExt.getRoleIds(), ',')); + List userInfoList = flowIdentityExtHelper.getUserInfoListByRoleIds(roleIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + } + + private void buildFlowTaskTimeoutExtensionData( + Map> attributeMap, JSONObject extraDataJson) { + List timeoutHandleWayAttributes = attributeMap.get(FlowConstant.TASK_TIMEOUT_HANDLE_WAY); + if (CollUtil.isNotEmpty(timeoutHandleWayAttributes)) { + String handleWay = timeoutHandleWayAttributes.get(0).getValue(); + extraDataJson.put(FlowConstant.TASK_TIMEOUT_HANDLE_WAY, handleWay); + List timeoutHoursAttributes = attributeMap.get(FlowConstant.TASK_TIMEOUT_HOURS); + if (CollUtil.isEmpty(timeoutHoursAttributes)) { + throw new MyRuntimeException("没有设置任务超时小时数!"); + } + Integer timeoutHours = Integer.valueOf(timeoutHoursAttributes.get(0).getValue()); + extraDataJson.put(FlowConstant.TASK_TIMEOUT_HOURS, timeoutHours); + if (StrUtil.equals(handleWay, FlowUserTaskExtData.TIMEOUT_AUTO_COMPLETE)) { + List defaultAssigneeAttributes = + attributeMap.get(FlowConstant.TASK_TIMEOUT_DEFAULT_ASSIGNEE); + if (CollUtil.isEmpty(defaultAssigneeAttributes)) { + throw new MyRuntimeException("没有设置超时任务处理人!"); + } + extraDataJson.put(FlowConstant.TASK_TIMEOUT_DEFAULT_ASSIGNEE, defaultAssigneeAttributes.get(0).getValue()); + } + } + } + + private void buildFlowTaskEmptyUserExtensionData( + Map> attributeMap, JSONObject extraDataJson) { + List emptyUserHandleWayAttributes = attributeMap.get(FlowConstant.EMPTY_USER_HANDLE_WAY); + if (CollUtil.isNotEmpty(emptyUserHandleWayAttributes)) { + String handleWay = emptyUserHandleWayAttributes.get(0).getValue(); + extraDataJson.put(FlowConstant.EMPTY_USER_HANDLE_WAY, handleWay); + if (StrUtil.equals(handleWay, FlowUserTaskExtData.EMPTY_USER_TO_ASSIGNEE)) { + List emptyUserToAssigneeAttributes = attributeMap.get(FlowConstant.EMPTY_USER_TO_ASSIGNEE); + if (CollUtil.isEmpty(emptyUserToAssigneeAttributes)) { + throw new MyRuntimeException("没有设置空审批人的指定处理人!"); + } + extraDataJson.put(FlowConstant.EMPTY_USER_TO_ASSIGNEE, emptyUserToAssigneeAttributes.get(0).getValue()); + } + } + } + + private JSONObject buildFlowTaskExtensionData(UserTask userTask) { + JSONObject extraDataJson = this.buildFlowElementExtToJson(userTask); + Map> attributeMap = userTask.getAttributes(); + if (MapUtil.isEmpty(attributeMap)) { + return extraDataJson; + } + if (extraDataJson == null) { + extraDataJson = new JSONObject(); + } + this.buildFlowTaskTimeoutExtensionData(attributeMap, extraDataJson); + this.buildFlowTaskEmptyUserExtensionData(attributeMap, extraDataJson); + List rejectTypeAttributes = attributeMap.get(FlowConstant.USER_TASK_REJECT_TYPE_KEY); + if (CollUtil.isNotEmpty(rejectTypeAttributes)) { + extraDataJson.put(FlowConstant.USER_TASK_REJECT_TYPE_KEY, rejectTypeAttributes.get(0).getValue()); + } + List sendMsgTypeAttributes = attributeMap.get("sendMessageType"); + if (CollUtil.isNotEmpty(sendMsgTypeAttributes)) { + ExtensionAttribute attribute = sendMsgTypeAttributes.get(0); + extraDataJson.put(FlowConstant.USER_TASK_NOTIFY_TYPES_KEY, StrUtil.split(attribute.getValue(), ",")); + } + return extraDataJson; + } + + private JSONObject buildUserCandidateGroupsExtensionElement(Map> extensionMap) { + JSONObject jsonData = null; + List elementCandidateGroupsList = extensionMap.get("userCandidateGroups"); + if (CollUtil.isEmpty(elementCandidateGroupsList)) { + return jsonData; + } + jsonData = new JSONObject(); + ExtensionElement ee = elementCandidateGroupsList.get(0); + jsonData.put(TYPE, ee.getAttributeValue(null, TYPE)); + jsonData.put(VALUE, ee.getAttributeValue(null, VALUE)); + return jsonData; + } + + private JSONObject buildAssigneeListExtensionElement(Map> extensionMap) { + JSONObject jsonData = null; + List elementAssigneeList = extensionMap.get("assigneeList"); + if (CollUtil.isEmpty(elementAssigneeList)) { + return jsonData; + } + ExtensionElement ee = elementAssigneeList.get(0); + Map> childExtensionMap = ee.getChildElements(); + if (MapUtil.isEmpty(childExtensionMap)) { + return jsonData; + } + List assigneeElements = childExtensionMap.get("assignee"); + if (CollUtil.isEmpty(assigneeElements)) { + return jsonData; + } + JSONArray assigneeIdArray = new JSONArray(); + for (ExtensionElement e : assigneeElements) { + assigneeIdArray.add(e.getAttributeValue(null, ID)); + } + jsonData = new JSONObject(); + String assigneeType = ee.getAttributeValue(null, TYPE); + jsonData.put("assigneeType", assigneeType); + jsonData.put("assigneeList", assigneeIdArray); + return jsonData; + } + + private List buildDeptPostListExtensionElement(Map> extensionMap) { + List deptPostElements = + this.getMyExtensionElementList(extensionMap, "deptPostList", "deptPost"); + if (CollUtil.isEmpty(deptPostElements)) { + return Collections.emptyList(); + } + List resultList = new LinkedList<>(); + for (ExtensionElement e : deptPostElements) { + JSONObject deptPostJsonData = new JSONObject(); + deptPostJsonData.put(ID, e.getAttributeValue(null, ID)); + deptPostJsonData.put(TYPE, e.getAttributeValue(null, TYPE)); + String postId = e.getAttributeValue(null, "postId"); + if (postId != null) { + deptPostJsonData.put("postId", postId); + } + String deptPostId = e.getAttributeValue(null, "deptPostId"); + if (deptPostId != null) { + deptPostJsonData.put("deptPostId", deptPostId); + } + resultList.add(deptPostJsonData); + } + return resultList; + } + + private List buildCopyListExtensionElement(Map> extensionMap) { + List copyElements = + this.getMyExtensionElementList(extensionMap, "copyItemList", "copyItem"); + if (CollUtil.isEmpty(copyElements)) { + return Collections.emptyList(); + } + List resultList = new LinkedList<>(); + for (ExtensionElement e : copyElements) { + JSONObject copyJsonData = new JSONObject(); + String type = e.getAttributeValue(null, TYPE); + copyJsonData.put(TYPE, type); + if (!StrUtil.equalsAny(type, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, + FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, + FlowConstant.GROUP_TYPE_USER_VAR, + FlowConstant.GROUP_TYPE_ROLE_VAR, + FlowConstant.GROUP_TYPE_DEPT_VAR, + FlowConstant.GROUP_TYPE_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR)) { + throw new MyRuntimeException("Invalid TYPE [" + type + " ] for CopyItenList Extension!"); + } + String id = e.getAttributeValue(null, ID); + if (StrUtil.isNotBlank(id)) { + copyJsonData.put(ID, id); + } + resultList.add(copyJsonData); + } + return resultList; + } + + private List getMyExtensionElementList( + Map> extensionMap, String rootName, String childName) { + if (extensionMap == null) { + return Collections.emptyList(); + } + List elementList = extensionMap.get(rootName); + if (CollUtil.isEmpty(elementList)) { + return Collections.emptyList(); + } + if (StrUtil.isBlank(childName)) { + return elementList; + } + ExtensionElement ee = elementList.get(0); + Map> childExtensionMap = ee.getChildElements(); + if (MapUtil.isEmpty(childExtensionMap)) { + return Collections.emptyList(); + } + List childrenElements = childExtensionMap.get(childName); + if (CollUtil.isEmpty(childrenElements)) { + return Collections.emptyList(); + } + return childrenElements; + } + + private List getAssigneeList(String executionId, String taskId) { + FlowMultiInstanceTrans flowMultiInstanceTrans = + flowMultiInstanceTransService.getByExecutionId(executionId, taskId); + String multiInstanceExecId; + if (flowMultiInstanceTrans == null) { + multiInstanceExecId = flowApiService.getTaskVariableStringWithSafe( + taskId, FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + } else { + multiInstanceExecId = flowMultiInstanceTrans.getMultiInstanceExecId(); + } + flowMultiInstanceTrans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + return flowMultiInstanceTrans == null ? null + : StrUtil.split(flowMultiInstanceTrans.getAssigneeList(), ","); + } + + private boolean buildTransferUserList(TaskInfo taskInfo, List resultUserMapList) { + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + List taskCommentList = flowTaskCommentService.getFlowTaskCommentListByExecutionId( + taskInfo.getProcessInstanceId(), taskInfo.getId(), taskInfo.getExecutionId()); + if (CollUtil.isEmpty(taskCommentList)) { + return false; + } + FlowTaskComment transferComment = null; + for (int i = taskCommentList.size() - 1; i >= 0; i--) { + FlowTaskComment comment = taskCommentList.get(i); + if (StrUtil.equalsAny(comment.getApprovalType(), + FlowApprovalType.TRANSFER, FlowApprovalType.INTERVENE)) { + transferComment = comment; + break; + } + } + if (transferComment == null || StrUtil.isBlank(transferComment.getDelegateAssignee())) { + return false; + } + Set loginNameSet = new HashSet<>(StrUtil.split(transferComment.getDelegateAssignee(), ",")); + resultUserMapList.addAll(flowIdentityExtHelper.getUserInfoListByUsernameSet(loginNameSet)); + return true; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java new file mode 100644 index 00000000..38e7aac6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java @@ -0,0 +1,354 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.dao.FlowWorkOrderExtMapper; +import com.orangeforms.common.flow.dao.FlowWorkOrderMapper; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.model.FlowEntry; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.FlowWorkOrderExt; +import com.orangeforms.common.flow.util.FlowOperationHelper; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowWorkOrderService") +public class FlowWorkOrderServiceImpl extends BaseService implements FlowWorkOrderService { + + @Autowired + private FlowWorkOrderMapper flowWorkOrderMapper; + @Autowired + private FlowWorkOrderExtMapper flowWorkOrderExtMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private FlowOperationHelper flowOperationHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowWorkOrderMapper; + } + + /** + * 保存新增对象。 + * + * @param instance 流程实例对象。 + * @param dataId 流程实例的BusinessKey。 + * @param onlineTableId 在线数据表的主键Id。 + * @param tableName 面向静态表单所使用的表名。 + * @return 新增的工作流工单对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowWorkOrder saveNew(ProcessInstance instance, Object dataId, Long onlineTableId, String tableName) { + // 正常插入流程工单数据。 + FlowWorkOrder flowWorkOrder = this.createWith(instance); + flowWorkOrder.setWorkOrderCode(this.generateWorkOrderCode(instance.getProcessDefinitionKey())); + flowWorkOrder.setBusinessKey(dataId.toString()); + flowWorkOrder.setOnlineTableId(onlineTableId); + flowWorkOrder.setTableName(tableName); + flowWorkOrder.setFlowStatus(FlowTaskStatus.SUBMITTED); + flowWorkOrderMapper.insert(flowWorkOrder); + return flowWorkOrder; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowWorkOrder saveNewWithDraft( + ProcessInstance instance, Long onlineTableId, String tableName, String masterData, String slaveData) { + FlowWorkOrder flowWorkOrder = this.createWith(instance); + flowWorkOrder.setWorkOrderCode(this.generateWorkOrderCode(instance.getProcessDefinitionKey())); + flowWorkOrder.setOnlineTableId(onlineTableId); + flowWorkOrder.setTableName(tableName); + flowWorkOrder.setFlowStatus(FlowTaskStatus.DRAFT); + JSONObject draftData = new JSONObject(); + if (masterData != null) { + draftData.put(FlowConstant.MASTER_DATA_KEY, masterData); + } + if (slaveData != null) { + draftData.put(FlowConstant.SLAVE_DATA_KEY, slaveData); + } + FlowWorkOrderExt flowWorkOrderExt = + BeanUtil.copyProperties(flowWorkOrder, FlowWorkOrderExt.class); + flowWorkOrderExt.setId(idGenerator.nextLongId()); + flowWorkOrderExt.setDraftData(JSON.toJSONString(draftData)); + flowWorkOrderExtMapper.insert(flowWorkOrderExt); + flowWorkOrderMapper.insert(flowWorkOrder); + return flowWorkOrder; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateDraft(Long workOrderId, String masterData, String slaveData) { + JSONObject draftData = new JSONObject(); + if (masterData != null) { + draftData.put(FlowConstant.MASTER_DATA_KEY, masterData); + } + if (slaveData != null) { + draftData.put(FlowConstant.SLAVE_DATA_KEY, slaveData); + } + FlowWorkOrderExt flowWorkOrderExt = new FlowWorkOrderExt(); + flowWorkOrderExt.setDraftData(JSON.toJSONString(draftData)); + flowWorkOrderExt.setUpdateTime(new Date()); + flowWorkOrderExtMapper.updateByQuery(flowWorkOrderExt, + new QueryWrapper().eq(FlowWorkOrderExt::getWorkOrderId, workOrderId)); + } + + /** + * 删除指定数据。 + * + * @param workOrderId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long workOrderId) { + return flowWorkOrderMapper.deleteById(workOrderId) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByProcessInstanceId(String processInstanceId) { + FlowWorkOrder filter = new FlowWorkOrder(); + filter.setProcessInstanceId(processInstanceId); + super.removeBy(filter); + } + + @Override + public List getFlowWorkOrderList(FlowWorkOrder filter, String orderBy) { + if (filter == null) { + filter = new FlowWorkOrder(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return flowWorkOrderMapper.getFlowWorkOrderList(filter, orderBy); + } + + @Override + public List getFlowWorkOrderListWithRelation(FlowWorkOrder filter, String orderBy) { + List resultList = this.getFlowWorkOrderList(filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + @Override + public FlowWorkOrder getFlowWorkOrderByProcessInstanceId(String processInstanceId) { + FlowWorkOrder filter = new FlowWorkOrder(); + filter.setProcessInstanceId(processInstanceId); + return flowWorkOrderMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + @Override + public boolean existByBusinessKey(String tableName, Object businessKey, boolean unfinished) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowWorkOrder::getBusinessKey, businessKey.toString()); + queryWrapper.eq(FlowWorkOrder::getTableName, tableName); + if (unfinished) { + queryWrapper.notIn(FlowWorkOrder::getFlowStatus, + FlowTaskStatus.FINISHED, FlowTaskStatus.CANCELLED, FlowTaskStatus.STOPPED); + } + return flowWorkOrderMapper.selectCountByQuery(queryWrapper) > 0; + } + + @Override + public boolean existUnfinished(String processDefinitionKey, Object businessKey) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowWorkOrder::getBusinessKey, businessKey.toString()); + queryWrapper.eq(FlowWorkOrder::getProcessDefinitionKey, processDefinitionKey); + queryWrapper.notIn(FlowWorkOrder::getFlowStatus, + FlowTaskStatus.FINISHED, FlowTaskStatus.CANCELLED, FlowTaskStatus.STOPPED); + return flowWorkOrderMapper.selectCountByQuery(queryWrapper) > 0; + } + + @DisableDataFilter + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFlowStatusByProcessInstanceId(String processInstanceId, Integer flowStatus) { + if (flowStatus == null) { + return; + } + FlowWorkOrder flowWorkOrder = new FlowWorkOrder(); + flowWorkOrder.setFlowStatus(flowStatus); + if (FlowTaskStatus.FINISHED != flowStatus) { + flowWorkOrder.setUpdateTime(new Date()); + flowWorkOrder.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + } + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq(FlowWorkOrder::getProcessInstanceId, processInstanceId); + flowWorkOrderMapper.updateByQuery(flowWorkOrder, queryWrapper); + } + + @DisableDataFilter + @Transactional(rollbackFor = Exception.class) + @Override + public void updateLatestApprovalStatusByProcessInstanceId(String processInstanceId, Integer approvalStatus) { + if (approvalStatus == null) { + return; + } + FlowWorkOrder flowWorkOrder = this.getFlowWorkOrderByProcessInstanceId(processInstanceId); + flowWorkOrder.setLatestApprovalStatus(approvalStatus); + flowWorkOrder.setUpdateTime(new Date()); + flowWorkOrder.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + flowWorkOrderMapper.update(flowWorkOrder); + // 处理在线表单工作流的自定义状态更新。 + flowCustomExtFactory.getOnlineBusinessDataExtHelper().updateFlowStatus(flowWorkOrder); + } + + @Override + public boolean hasDataPermOnFlowWorkOrder(String processInstanceId) { + // 开启数据权限,并进行验证。 + boolean originalFlag = GlobalThreadLocal.setDataFilter(true); + long count; + try { + FlowWorkOrder filter = new FlowWorkOrder(); + filter.setProcessInstanceId(processInstanceId); + count = flowWorkOrderMapper.selectCountByQuery(QueryWrapper.create(filter)); + } finally { + // 恢复之前的数据权限标记 + GlobalThreadLocal.setDataFilter(originalFlag); + } + return count > 0; + } + + @Override + public void fillUserShowNameByLoginName(List dataList) { + BaseFlowIdentityExtHelper identityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + Set loginNameSet = dataList.stream() + .map(FlowWorkOrderVo::getSubmitUsername).collect(Collectors.toSet()); + if (CollUtil.isEmpty(loginNameSet)) { + return; + } + Map userNameMap = identityExtHelper.mapUserShowNameByLoginName(loginNameSet); + dataList.forEach(workOrder -> { + if (StrUtil.isNotBlank(workOrder.getSubmitUsername())) { + workOrder.setUserShowName(userNameMap.get(workOrder.getSubmitUsername())); + } + }); + } + + @Override + public FlowWorkOrderExt getFlowWorkOrderExtByWorkOrderId(Long workOrderId) { + return flowWorkOrderExtMapper.selectOneByQuery( + new QueryWrapper().eq(FlowWorkOrderExt::getWorkOrderId, workOrderId)); + } + + @Override + public List getFlowWorkOrderExtByWorkOrderIds(Set workOrderIds) { + return flowWorkOrderExtMapper.selectListByQuery( + new QueryWrapper().in(FlowWorkOrderExt::getWorkOrderId, workOrderIds)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult removeDraft(FlowWorkOrder flowWorkOrder) { + CallResult r = flowApiService.stopProcessInstance(flowWorkOrder.getProcessInstanceId(), "撤销草稿", true); + if (!r.isSuccess()) { + return r; + } + flowWorkOrderMapper.deleteById(flowWorkOrder.getWorkOrderId()); + return CallResult.ok(); + } + + @Override + public MyPageData getPagedWorkOrderListAndBuildData( + FlowWorkOrderDto flowWorkOrderDtoFilter, MyPageParam pageParam, MyOrderParam orderParam, String processDefinitionKey) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowWorkOrder.class); + FlowWorkOrder filter = flowOperationHelper.makeWorkOrderFilter(flowWorkOrderDtoFilter, processDefinitionKey); + List flowWorkOrderList = this.getFlowWorkOrderList(filter, orderBy); + MyPageData resultData = + MyPageUtil.makeResponseData(flowWorkOrderList, FlowWorkOrderVo.class); + if (CollUtil.isEmpty(resultData.getDataList())) { + return resultData; + } + flowOperationHelper.buildWorkOrderApprovalStatus(processDefinitionKey, resultData.getDataList()); + // 根据工单的提交用户名获取用户的显示名称,便于前端显示。 + // 同时这也是一个如何通过插件方法,将loginName映射到showName的示例, + this.fillUserShowNameByLoginName(resultData.getDataList()); + // 组装工单中需要返回给前端的流程任务数据。 + flowOperationHelper.buildWorkOrderTaskInfo(resultData.getDataList()); + return resultData; + } + + private FlowWorkOrder createWith(ProcessInstance instance) { + TokenData tokenData = TokenData.takeFromRequest(); + Date now = new Date(); + FlowWorkOrder flowWorkOrder = new FlowWorkOrder(); + flowWorkOrder.setWorkOrderId(idGenerator.nextLongId()); + flowWorkOrder.setProcessDefinitionKey(instance.getProcessDefinitionKey()); + flowWorkOrder.setProcessDefinitionName(instance.getProcessDefinitionName()); + flowWorkOrder.setProcessDefinitionId(instance.getProcessDefinitionId()); + flowWorkOrder.setProcessInstanceId(instance.getId()); + flowWorkOrder.setSubmitUsername(tokenData.getLoginName()); + flowWorkOrder.setDeptId(tokenData.getDeptId()); + flowWorkOrder.setAppCode(tokenData.getAppCode()); + flowWorkOrder.setTenantId(tokenData.getTenantId()); + flowWorkOrder.setCreateUserId(tokenData.getUserId()); + flowWorkOrder.setUpdateUserId(tokenData.getUserId()); + flowWorkOrder.setCreateTime(now); + flowWorkOrder.setUpdateTime(now); + flowWorkOrder.setDeletedFlag(GlobalDeletedFlag.NORMAL); + return flowWorkOrder; + } + + private String generateWorkOrderCode(String processDefinitionKey) { + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (StrUtil.isBlank(flowEntry.getEncodedRule())) { + return null; + } + ColumnEncodedRule rule = JSON.parseObject(flowEntry.getEncodedRule(), ColumnEncodedRule.class); + if (rule.getIdWidth() == null) { + rule.setIdWidth(10); + } + return commonRedisUtil.generateTransId( + rule.getPrefix(), rule.getPrecisionTo(), rule.getMiddle(), rule.getIdWidth()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java new file mode 100644 index 00000000..30715c8c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java @@ -0,0 +1,253 @@ +package com.orangeforms.common.flow.util; + +import com.orangeforms.common.flow.listener.DeptPostLeaderListener; +import com.orangeforms.common.flow.listener.UpDeptPostLeaderListener; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import org.flowable.engine.delegate.TaskListener; + +import java.util.*; + +/** + * 工作流与用户身份相关的自定义扩展接口,需要业务模块自行实现该接口。也可以根据实际需求扩展该接口的方法。 + * 目前支持的主键类型为字符型和长整型,所以这里提供了两套实现接口。可根据实际情况实现其中一套即可。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseFlowIdentityExtHelper { + + /** + * 根据(字符型)部门Id,获取当前用户部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户部门领导所有的部门岗位Id。 + */ + default String getLeaderDeptPostId(String deptId) { + return null; + } + + /** + * 根据(字符型)部门Id,获取当前用户上级部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户上级部门领导所有的部门岗位Id。 + */ + default String getUpLeaderDeptPostId(String deptId) { + return null; + } + + /** + * 获取(字符型)指定部门上级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与该部门Id上级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getUpDeptPostIdMap(String deptId, Set postIdSet) { + return null; + } + + /** + * 获取(字符型)指定部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getDeptPostIdMap(String deptId, Set postIdSet) { + return null; + } + + /** + * 获取(字符型)指定同级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的同级部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与同级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getSiblingDeptPostIdMap(String deptId, Set postIdSet) { + return null; + } + + /** + * 根据(长整型)部门Id,获取当前用户部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户部门领导所有的部门岗位Id。 + */ + default Long getLeaderDeptPostId(Long deptId) { + return null; + } + + /** + * 根据(长整型)部门Id,获取当前用户上级部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户上级部门领导所有的部门岗位Id。 + */ + default Long getUpLeaderDeptPostId(Long deptId) { + return null; + } + + /** + * 获取(长整型)指定部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getDeptPostIdMap(Long deptId, Set postIdSet) { + return null; + } + + /** + * 获取(长整型)指定同级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的同级部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与同级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getSiblingDeptPostIdMap(Long deptId, Set postIdSet) { + return null; + } + + /** + * 获取(长整型)指定部门上级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与该部门Id上级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getUpDeptPostIdMap(Long deptId, Set postIdSet) { + return null; + } + + /** + * 根据角色Id集合,查询所属的用户名列表。 + * + * @param roleIdSet 角色Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByRoleIds(Set roleIdSet) { + return Collections.emptySet(); + } + + /** + * 根据角色Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param roleIdSet 角色Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByRoleIds(Set roleIdSet) { + return Collections.emptyList(); + } + + /** + * 根据部门Id集合,查询所属的用户名列表。 + * + * @param deptIdSet 部门Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByDeptIds(Set deptIdSet) { + return Collections.emptySet(); + } + + /** + * 根据部门Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param deptIdSet 部门Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByDeptIds(Set deptIdSet) { + return Collections.emptyList(); + } + + /** + * 根据岗位Id集合,查询所属的用户名列表。 + * + * @param postIdSet 岗位Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByPostIds(Set postIdSet) { + return Collections.emptySet(); + } + + /** + * 根据岗位Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param postIdSet 岗位Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByPostIds(Set postIdSet) { + return Collections.emptyList(); + } + + /** + * 根据部门岗位Id集合,查询所属的用户名列表。 + * + * @param deptPostIdSet 部门岗位Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByDeptPostIds(Set deptPostIdSet) { + return Collections.emptySet(); + } + + /** + * 根据部门岗位Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param deptPostIdSet 部门岗位Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByDeptPostIds(Set deptPostIdSet) { + return Collections.emptyList(); + } + + /** + * 根据用户登录名集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param usernameSet 用户登录名集合。 + * @return 用户对象信息列表。 + */ + default List getUserInfoListByUsernameSet(Set usernameSet) { + return Collections.emptyList(); + } + + /** + * 当前服务是否支持数据权限。 + * + * @return true表示支持,否则false。 + */ + default Boolean supprtDataPerm() { + return false; + } + + /** + * 映射用户的登录名到用户的显示名。 + * + * @param loginNameSet 用户登录名集合。 + * @return 用户登录名和显示名的Map,key为登录名,value是显示名。 + */ + default Map mapUserShowNameByLoginName(Set loginNameSet) { + return new HashMap<>(1); + } + + /** + * 获取任务执行人是当前部门领导岗位的任务监听器。 + * 通常会在没有找到领导部门岗位Id的时候,为当前任务指定其他的指派人、候选人或候选组。 + * + * @return 任务监听器。 + */ + default Class getDeptPostLeaderListener() { + return DeptPostLeaderListener.class; + } + + /** + * 获取任务执行人是上级部门领导岗位的任务监听器。 + * 通常会在没有找到领导部门岗位Id的时候,为当前任务指定其他的指派人、候选人或候选组。 + * + * @return 任务监听器。 + */ + default Class getUpDeptPostLeaderListener() { + return UpDeptPostLeaderListener.class; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java new file mode 100644 index 00000000..d90cd432 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.flow.util; + +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 流程通知扩展帮助实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class BaseFlowNotifyExtHelper { + + /** + * 处理消息。 + * + * @param notifyType 通知类型,具体值可参考FlowUserTaskExtData中NOTIFY_TYPE开头的常量。 + * @param userInfoList 待通知的用户信息列表。 + */ + public void doNotify(String notifyType, List userInfoList, FlowTaskVo taskInfo) { + userInfoList.forEach(u -> log.info( + "The user [{}] of Task [{}] is notified by [{}].", u.getLoginName(), taskInfo.getTaskKey(), notifyType)); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java new file mode 100644 index 00000000..76b0e2cb --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java @@ -0,0 +1,51 @@ +package com.orangeforms.common.flow.util; + +import cn.hutool.core.lang.Assert; +import com.orangeforms.common.flow.base.service.BaseFlowOnlineService; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import lombok.extern.slf4j.Slf4j; + +/** + * 面向在线表单工作流的业务数据扩展帮助实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class BaseOnlineBusinessDataExtHelper { + + private BaseFlowOnlineService onlineBusinessService; + + /** + * 设置在线表单的业务处理服务。 + * + * @param onlineBusinessService 在线表单业务处理服务实现类。 + */ + public void setOnlineBusinessService(BaseFlowOnlineService onlineBusinessService) { + this.onlineBusinessService = onlineBusinessService; + } + + /** + * 更新在线表单主表数据的流程状态字段值。 + * + * @param workOrder 工单对象。 + */ + public void updateFlowStatus(FlowWorkOrder workOrder) { + Assert.notNull(workOrder.getOnlineTableId()); + if (this.onlineBusinessService != null && workOrder.getBusinessKey() != null) { + onlineBusinessService.updateFlowStatus(workOrder); + } + } + + /** + * 根据工单对象级联删除业务数据。 + * + * @param workOrder 工单对象。 + */ + public void deleteBusinessData(FlowWorkOrder workOrder) { + Assert.notNull(workOrder.getOnlineTableId()); + if (this.onlineBusinessService != null && workOrder.getBusinessKey() != null) { + onlineBusinessService.deleteBusinessData(workOrder); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java new file mode 100644 index 00000000..aa05956c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.flow.util; + +import org.flowable.engine.impl.RuntimeServiceImpl; +import org.flowable.engine.impl.runtime.ChangeActivityStateBuilderImpl; +import org.flowable.engine.runtime.ChangeActivityStateBuilder; + +import java.util.List; + +/** + * 自定义修改活动状态构建器实现。主要用于支持多个源节点向多个目标节点跳转的功能。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class CustomChangeActivityStateBuilderImpl extends ChangeActivityStateBuilderImpl { + + public CustomChangeActivityStateBuilderImpl() { + super(); + } + + public CustomChangeActivityStateBuilderImpl(RuntimeServiceImpl runtimeService) { + super(runtimeService); + } + + public ChangeActivityStateBuilder moveActivityIdsToActivityIds(List activityIds, List moveToActivityIds) { + moveActivityIdList.add(new CustomMoveActivityIdContainer(activityIds, moveToActivityIds)); + return this; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java new file mode 100644 index 00000000..66fa2e7e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.flow.util; + +import org.flowable.engine.impl.runtime.MoveActivityIdContainer; + +import java.util.List; + +/** + * 自定义移动任务Id的容器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class CustomMoveActivityIdContainer extends MoveActivityIdContainer { + + public CustomMoveActivityIdContainer(String singleActivityId, String moveToActivityId) { + super(singleActivityId, moveToActivityId); + } + + public CustomMoveActivityIdContainer(List activityIds, List moveToActivityIds) { + super(activityIds.get(0), moveToActivityIds.get(0)); + this.activityIds = activityIds; + this.moveToActivityIds = moveToActivityIds; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java new file mode 100644 index 00000000..422e016a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java @@ -0,0 +1,67 @@ +package com.orangeforms.common.flow.util; + +import org.springframework.stereotype.Component; + +/** + * 工作流自定义扩展工厂类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class FlowCustomExtFactory { + + private BaseFlowIdentityExtHelper flowIdentityExtHelper; + + private BaseOnlineBusinessDataExtHelper onlineBusinessDataExtHelper = new BaseOnlineBusinessDataExtHelper(); + + private BaseFlowNotifyExtHelper flowNotifyExtHelper; + + /** + * 获取业务模块自行实现的用户身份相关的扩展帮助实现类。 + * + * @return 业务模块自行实现的用户身份相关的扩展帮助实现类。 + */ + public BaseFlowIdentityExtHelper getFlowIdentityExtHelper() { + return flowIdentityExtHelper; + } + + /** + * 注册业务模块自行实现的用户身份扩展帮助实现类。 + * + * @param helper 业务模块自行实现的用户身份扩展帮助实现类。 + */ + public void registerFlowIdentityExtHelper(BaseFlowIdentityExtHelper helper) { + this.flowIdentityExtHelper = helper; + } + + /** + * 获取有关在线表单业务数据的扩展帮助实现类。 + * + * @return 有关业务数据的扩展帮助实现类。 + */ + public BaseOnlineBusinessDataExtHelper getOnlineBusinessDataExtHelper() { + return onlineBusinessDataExtHelper; + } + + /** + * 注册流程通知扩展帮助实现类。 + * + * @param helper 流程通知扩展帮助实现类。 + */ + public void registerNotifyExtHelper(BaseFlowNotifyExtHelper helper) { + this.flowNotifyExtHelper = helper; + } + + /** + * 获取流程通知扩展帮助实现类。 + * + * @return 流程消息通知扩展帮助实现类。 + */ + public BaseFlowNotifyExtHelper getFlowNotifyExtHelper() { + if (this.flowNotifyExtHelper == null) { + this.flowNotifyExtHelper = new BaseFlowNotifyExtHelper(); + } + return flowNotifyExtHelper; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java new file mode 100644 index 00000000..3b3ebc8e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java @@ -0,0 +1,505 @@ +package com.orangeforms.common.flow.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.dto.FlowTaskCommentDto; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.model.FlowEntry; +import com.orangeforms.common.flow.model.FlowEntryPublish; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.object.FlowEntryExtensionData; +import com.orangeforms.common.flow.object.FlowRumtimeObject; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 工作流操作的通用帮助对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class FlowOperationHelper { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + + /** + * 验证并获取流程对象。 + * + * @param processDefinitionKey 流程引擎的流程定义标识。 + * @return 流程对象。 + */ + public ResponseResult verifyAndGetFlowEntry(String processDefinitionKey) { + String errorMessage; + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (flowEntry == null) { + errorMessage = "数据验证失败,该流程并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!flowEntry.getStatus().equals(FlowEntryStatus.PUBLISHED)) { + errorMessage = "数据验证失败,该流程尚未发布,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntryPublish flowEntryPublish = + flowEntryService.getFlowEntryPublishFromCache(flowEntry.getMainEntryPublishId()); + flowEntry.setMainFlowEntryPublish(flowEntryPublish); + return ResponseResult.success(flowEntry); + } + + /** + * 验证并获取流程发布对象。 + * + * @param processDefinitionKey 流程引擎的流程定义标识。 + * @return 流程对象。 + */ + public ResponseResult verifyAndGetFlowEntryPublish(String processDefinitionKey) { + // 1. 验证流程数据的合法性。 + ResponseResult flowEntryResult = this.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + // 2. 验证流程一个用户任务的合法性。 + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + ResponseResult taskInfoResult = this.verifyAndGetInitialTaskInfo(flowEntryPublish, false); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + return ResponseResult.success(flowEntryPublish); + } + + /** + * 工作流静态表单的参数验证工具方法。根据流程定义标识,获取关联的流程并对其进行合法性验证。 + * + * @param processDefinitionKey 流程定义标识。 + * @return 返回流程对象。 + */ + public ResponseResult verifyFullAndGetFlowEntry(String processDefinitionKey) { + String errorMessage; + // 验证流程管理数据状态的合法性。 + ResponseResult flowEntryResult = this.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + // 验证流程一个用户任务的合法性。 + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + if (BooleanUtil.isFalse(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布对象已被挂起,不能启动新流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult taskInfoResult = + this.verifyAndGetInitialTaskInfo(flowEntryPublish, true); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + return flowEntryResult; + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的流程任务对象。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批对象。 + * @return 验证后的流程任务对象。 + */ + public ResponseResult verifySubmitAndGetTask( + String processInstanceId, String taskId, FlowTaskCommentDto flowTaskComment) { + // 验证流程任务的合法性。 + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ResponseResult taskInfoResult = this.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + CallResult assigneeVerifyResult = flowApiService.verifyAssigneeOrCandidateAndClaim(task); + if (!assigneeVerifyResult.isSuccess()) { + return ResponseResult.errorFrom(assigneeVerifyResult); + } + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + if (StrUtil.isBlank(instance.getBusinessKey())) { + return ResponseResult.success(task); + } + String errorMessage; + if (flowTaskComment != null + && StrUtil.equals(flowTaskComment.getApprovalType(), FlowApprovalType.TRANSFER) + && StrUtil.isBlank(flowTaskComment.getDelegateAssignee())) { + errorMessage = "数据验证失败,加签或转办任务指派人不能为空!!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(task); + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的流程任务和流程实例。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批对象。 + * @param processDefinitionKey 流程定义标识。 + * @return 验证后的流程运行时常用对象。 + */ + public ResponseResult verifySubmitWithGetInstanceAndTask( + String processInstanceId, String taskId, FlowTaskCommentDto flowTaskComment, String processDefinitionKey) { + ResponseResult taskResult = this.verifySubmitAndGetTask(processInstanceId, taskId, flowTaskComment); + if (!taskResult.isSuccess()) { + return ResponseResult.errorFrom(taskResult); + } + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + if (!StrUtil.equals(instance.getProcessDefinitionKey(), processDefinitionKey)) { + String errorMessage = "数据验证失败,请求流程标识与流程实例不匹配,请核对!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowRumtimeObject o = new FlowRumtimeObject(); + o.setTask(taskResult.getData()); + o.setInstance(instance); + return ResponseResult.success(o); + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的历史流程实例对象。 + * 仅当登录用户为任务的分配人时,才能通过验证。 + * + * @param processInstanceId 历史流程实例Id。 + * @param taskId 历史流程任务Id。 + * @return 验证后并返回的历史流程实例对象。 + */ + public ResponseResult verifyAndGetHistoricProcessInstance(String processInstanceId, String taskId) { + String errorMessage; + // 验证流程实例的合法性。 + HistoricProcessInstance instance = flowApiService.getHistoricProcessInstance(processInstanceId); + if (instance == null) { + errorMessage = "数据验证失败,指定的流程实例Id并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String loginName = TokenData.takeFromRequest().getLoginName(); + if (StrUtil.isBlank(taskId)) { + if (!StrUtil.equals(loginName, instance.getStartUserId()) + && !flowWorkOrderService.hasDataPermOnFlowWorkOrder(processInstanceId)) { + errorMessage = "数据验证失败,指定历史流程的发起人与当前用户不匹配,或者没有查看该工单详情的数据权限!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } else { + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (taskInstance == null) { + errorMessage = "数据验证失败,指定的任务Id并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(loginName, taskInstance.getAssignee()) + && !flowWorkOrderService.hasDataPermOnFlowWorkOrder(processInstanceId)) { + errorMessage = "数据验证失败,历史任务的指派人与当前用户不匹配,或者没有查看该工单详情的数据权限!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(instance); + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的历史流程实例对象。 + * 仅当登录用户为任务的分配人时,才能通过验证。 + * + * @param processInstanceId 历史流程实例Id。 + * @param taskId 历史流程任务Id。 + * @param processDefinitionKey 流程定义标识。 + * @return 验证后并返回的历史流程实例对象。 + */ + public ResponseResult verifyAndGetHistoricProcessInstance( + String processInstanceId, String taskId, String processDefinitionKey) { + ResponseResult instanceResult = + this.verifyAndGetHistoricProcessInstance(processInstanceId, taskId); + if (!instanceResult.isSuccess()) { + return ResponseResult.errorFrom(instanceResult); + } + HistoricProcessInstance instance = instanceResult.getData(); + if (!StrUtil.equals(instance.getProcessDefinitionKey(), processDefinitionKey)) { + String errorMessage = "数据验证失败,请求流程标识与流程实例不匹配,请核对!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(instance); + } + + /** + * 验证并获取流程的实时任务信息。 + * + * @param task 流程引擎的任务对象。 + * @return 任务信息对象。 + */ + public ResponseResult verifyAndGetRuntimeTaskInfo(Task task) { + String errorMessage; + if (task == null) { + errorMessage = "数据验证失败,指定的任务Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowApiService.isAssigneeOrCandidate(task)) { + errorMessage = "数据验证失败,当前用户不是指派人也不是候选人之一!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (StrUtil.isBlank(task.getFormKey())) { + errorMessage = "数据验证失败,指定任务的formKey属性不存在,请重新修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfo = JSON.parseObject(task.getFormKey(), TaskInfoVo.class); + taskInfo.setTaskKey(task.getTaskDefinitionKey()); + return ResponseResult.success(taskInfo); + } + + /** + * 验证并获取启动任务的对象信息。 + * + * @param flowEntryPublish 流程发布对象。 + * @param checkStarter 是否检查发起用户。 + * @return 第一个可执行的任务信息。 + */ + public ResponseResult verifyAndGetInitialTaskInfo( + FlowEntryPublish flowEntryPublish, boolean checkStarter) { + String errorMessage; + if (StrUtil.isBlank(flowEntryPublish.getInitTaskInfo())) { + errorMessage = "数据验证失败,当前流程发布的数据中,没有包含初始任务信息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfo = JSON.parseObject(flowEntryPublish.getInitTaskInfo(), TaskInfoVo.class); + if (checkStarter) { + String loginName = TokenData.takeFromRequest().getLoginName(); + if (!StrUtil.equalsAny(taskInfo.getAssignee(), loginName, FlowConstant.START_USER_NAME_VAR)) { + errorMessage = "数据验证失败,该工作流第一个用户任务的指派人并非当前用户,不能执行该操作!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(taskInfo); + } + + /** + * 判断当前用户是否有当前流程实例的数据上传或下载权限。 + * 如果taskId为空,则验证当前用户是否为当前流程实例的发起人,否则判断是否为当前任务的指派人或候选人。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @return 验证结果。 + */ + public ResponseResult verifyUploadOrDownloadPermission(String processInstanceId, String taskId) { + if (flowApiService.isProcessInstanceStarter(processInstanceId)) { + return ResponseResult.success(); + } + String errorMessage; + if (StrUtil.isBlank(taskId)) { + errorMessage = "数据验证失败,当前用户没有权限下载!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + TaskInfo task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + if (task == null) { + task = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (task == null) { + errorMessage = "数据验证失败,指定任务Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + } + if (!flowApiService.isAssigneeOrCandidate(task)) { + errorMessage = "数据验证失败,当前用户并非指派人或候选人,因此没有权限下载!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 根据已有的过滤对象,补充添加缺省过滤条件。如流程标识、创建用户等。 + * + * @param filterDto 工单过滤对象。 + * @param processDefinitionKey 流程标识。 + * @return 创建并转换后的流程工单过滤对象。 + */ + public FlowWorkOrder makeWorkOrderFilter(FlowWorkOrderDto filterDto, String processDefinitionKey) { + FlowWorkOrder filter = MyModelUtil.copyTo(filterDto, FlowWorkOrder.class); + if (filter == null) { + filter = new FlowWorkOrder(); + } + filter.setProcessDefinitionKey(processDefinitionKey); + // 下面的方法会帮助构建工单的数据权限过滤条件,和业务希望相比,如果当前系统没有支持数据权限, + // 用户则只能看到自己发起的工单,否则按照数据权限过滤。然而需要特殊处理的是,如果用户的数据 + // 权限中,没有包含能看自己,这里也需要自动给加上。 + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + if (BooleanUtil.isFalse(flowIdentityExtHelper.supprtDataPerm())) { + filter.setCreateUserId(TokenData.takeFromRequest().getUserId()); + } + return filter; + } + + /** + * 组装工作流工单列表中的流程任务数据。 + * + * @param flowWorkOrderVoList 工作流工单列表。 + */ + public void buildWorkOrderTaskInfo(List flowWorkOrderVoList) { + if (CollUtil.isEmpty(flowWorkOrderVoList)) { + return; + } + Set definitionIdSet = + flowWorkOrderVoList.stream().map(FlowWorkOrderVo::getProcessDefinitionId).collect(Collectors.toSet()); + List flowEntryPublishList = flowEntryService.getFlowEntryPublishList(definitionIdSet); + Map flowEntryPublishMap = + flowEntryPublishList.stream().collect(Collectors.toMap(FlowEntryPublish::getProcessDefinitionId, c -> c)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + FlowEntryPublish flowEntryPublish = flowEntryPublishMap.get(flowWorkOrderVo.getProcessDefinitionId()); + flowWorkOrderVo.setInitTaskInfo(flowEntryPublish.getInitTaskInfo()); + } + List unfinishedProcessInstanceIds = flowWorkOrderVoList.stream() + .filter(c -> !c.getFlowStatus().equals(FlowTaskStatus.FINISHED)) + .map(FlowWorkOrderVo::getProcessInstanceId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(unfinishedProcessInstanceIds)) { + return; + } + List taskList = flowApiService.getTaskListByProcessInstanceIds(unfinishedProcessInstanceIds); + Map> taskMap = + taskList.stream().collect(Collectors.groupingBy(Task::getProcessInstanceId)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + List instanceTaskList = taskMap.get(flowWorkOrderVo.getProcessInstanceId()); + if (instanceTaskList == null) { + continue; + } + JSONArray taskArray = new JSONArray(); + for (Task task : instanceTaskList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("taskId", task.getId()); + jsonObject.put("taskName", task.getName()); + jsonObject.put("taskKey", task.getTaskDefinitionKey()); + jsonObject.put("assignee", task.getAssignee()); + taskArray.add(jsonObject); + } + flowWorkOrderVo.setRuntimeTaskInfoList(taskArray); + } + } + + /** + * 组装工作流工单中的业务数据。 + * + * @param workOrderVoList 工单列表。 + * @param dataList 业务数据列表。 + * @param idGetter 获取业务对象主键字段的返回方法。 + * @param 业务主对象类型。 + * @param 业务主对象的主键字段类型。 + */ + public void buildWorkOrderBusinessData( + List workOrderVoList, List dataList, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return; + } + Map dataMap = dataList.stream().collect(Collectors.toMap(idGetter, c -> c)); + K id = idGetter.apply(dataList.get(0)); + for (FlowWorkOrderVo flowWorkOrderVo : workOrderVoList) { + if (StrUtil.isBlank(flowWorkOrderVo.getBusinessKey())) { + continue; + } + Object dataId = flowWorkOrderVo.getBusinessKey(); + if (id instanceof Long) { + dataId = Long.valueOf(flowWorkOrderVo.getBusinessKey()); + } else if (id instanceof Integer) { + dataId = Integer.valueOf(flowWorkOrderVo.getBusinessKey()); + } + T data = dataMap.get(dataId); + if (data != null) { + flowWorkOrderVo.setMasterData(BeanUtil.beanToMap(data)); + } + } + } + + /** + * 验证并根据流程实例Id获取处于草稿状态的流程工单。 + * + * @param processDefinitionKey 流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @return 流程工单。 + */ + public ResponseResult verifyAndGetFlowWorkOrderWithDraft( + String processDefinitionKey, String processInstanceId) { + String errorMessage; + FlowWorkOrder flowWorkOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); + if (flowWorkOrder == null) { + errorMessage = "数据验证失败,流程实例关联的工单不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + errorMessage = "数据验证失败,当前流程工单并不处于草稿保存状态!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowWorkOrder.getCreateUserId().equals(TokenData.takeFromRequest().getUserId())) { + errorMessage = "数据验证失败,草稿数据保存用户与当前用户不一致!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (processDefinitionKey != null && !flowWorkOrder.getProcessDefinitionKey().equals(processDefinitionKey)) { + errorMessage = "数据验证失败,流程实例和流程定义标识不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(flowWorkOrder); + } + + /** + * 根据流程定义的扩展数据中的审批状态字典列表数据,组装工单列表中,每个工单对象的审批状态字典数据。 + * @param processDefinitionKey 流程定义标识。 + * @param workOrderVoList 待组装的工单列表。 + */ + public void buildWorkOrderApprovalStatus(String processDefinitionKey, List workOrderVoList) { + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (StrUtil.isBlank(flowEntry.getExtensionData())) { + return; + } + FlowEntryExtensionData extensionData = + JSON.parseObject(flowEntry.getExtensionData(), FlowEntryExtensionData.class); + if (CollUtil.isEmpty(extensionData.getApprovalStatusDict())) { + return; + } + Map dictMap = new HashMap<>(extensionData.getApprovalStatusDict().size()); + for (Map m : extensionData.getApprovalStatusDict()) { + dictMap.put(Integer.valueOf(m.get("id")), m.get("name")); + } + for (FlowWorkOrderVo workOrderVo : workOrderVoList) { + if (workOrderVo.getLatestApprovalStatus() != null) { + String name = dictMap.get(workOrderVo.getLatestApprovalStatus()); + if (name != null) { + Map lastestApprovalStatusDictMap = MapUtil.newHashMap(); + lastestApprovalStatusDictMap.put("id", workOrderVo.getLatestApprovalStatus()); + lastestApprovalStatusDictMap.put("name", name); + workOrderVo.setLatestApprovalStatusDictMap(lastestApprovalStatusDictMap); + } + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java new file mode 100644 index 00000000..b95cd08e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.flow.util; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.object.TokenData; + +/** + * 工作流 Redis 键生成工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowRedisKeyUtil { + + /** + * 计算流程对象缓存在Redis中的键值。 + * + * @param processDefinitionKey 流程标识。 + * @return 流程对象缓存在Redis中的键值。 + */ + public static String makeFlowEntryKey(String processDefinitionKey) { + String prefix = "FLOW_ENTRY:"; + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData == null) { + return prefix + processDefinitionKey; + } + String appCode = tokenData.getAppCode(); + if (StrUtil.isBlank(appCode)) { + Long tenantId = tokenData.getTenantId(); + if (tenantId == null) { + return prefix + processDefinitionKey; + } + return prefix + tenantId.toString() + ":" + processDefinitionKey; + } + return prefix + appCode + ":" + processDefinitionKey; + } + + /** + * 流程发布对象缓存在Redis中的键值。 + * + * @param flowEntryPublishId 流程发布主键Id。 + * @return 流程发布对象缓存在Redis中的键值。 + */ + public static String makeFlowEntryPublishKey(Long flowEntryPublishId) { + return "FLOW_ENTRY_PUBLISH:" + flowEntryPublishId; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowRedisKeyUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java new file mode 100644 index 00000000..56894a81 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程分类的Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程分类的Vo对象") +@Data +public class FlowCategoryVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long categoryId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码") + private String appCode; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称") + private String name; + + /** + * 分类编码。 + */ + @Schema(description = "分类编码") + private String code; + + /** + * 实现顺序。 + */ + @Schema(description = "实现顺序") + private Integer showOrder; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java new file mode 100644 index 00000000..53c802fa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程发布信息的Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程发布信息的Vo对象") +@Data +public class FlowEntryPublishVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long entryPublishId; + + /** + * 发布版本。 + */ + @Schema(description = "发布版本") + private Integer publishVersion; + + /** + * 流程引擎中的流程定义Id。 + */ + @Schema(description = "流程引擎中的流程定义Id") + private String processDefinitionId; + + /** + * 激活状态。 + */ + @Schema(description = "激活状态") + private Boolean activeStatus; + + /** + * 是否为主版本。 + */ + @Schema(description = "是否为主版本") + private Boolean mainVersion; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 发布时间。 + */ + @Schema(description = "发布时间") + private Date publishTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java new file mode 100644 index 00000000..68ef4d33 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程变量Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程变量Vo对象") +@Data +public class FlowEntryVariableVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long variableId; + + /** + * 流程Id。 + */ + @Schema(description = "流程Id") + private Long entryId; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + private String variableName; + + /** + * 显示名。 + */ + @Schema(description = "显示名") + private String showName; + + /** + * 变量类型。 + */ + @Schema(description = "变量类型") + private Integer variableType; + + /** + * 绑定数据源Id。 + */ + @Schema(description = "绑定数据源Id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @Schema(description = "绑定数据源关联Id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @Schema(description = "绑定字段Id") + private Long bindColumnId; + + /** + * 是否内置。 + */ + @Schema(description = "是否内置") + private Boolean builtin; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java new file mode 100644 index 00000000..b9cdc945 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java @@ -0,0 +1,157 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 流程的Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程的Vo对象") +@Data +public class FlowEntryVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long entryId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码") + private String appCode; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程标识Key。 + */ + @Schema(description = "流程标识Key") + private String processDefinitionKey; + + /** + * 流程分类。 + */ + @Schema(description = "流程分类") + private Long categoryId; + + /** + * 工作流部署的发布主版本Id。 + */ + @Schema(description = "工作流部署的发布主版本Id") + private Long mainEntryPublishId; + + /** + * 最新发布时间。 + */ + @Schema(description = "最新发布时间") + private Date latestPublishTime; + + /** + * 流程状态。 + */ + @Schema(description = "流程状态") + private Integer status; + + /** + * 流程定义的xml。 + */ + @Schema(description = "流程定义的xml") + private String bpmnXml; + + /** + * 流程图类型。0: 普通流程图,1: 钉钉风格的流程图。 + */ + @Schema(description = "流程图类型。0: 普通流程图,1: 钉钉风格的流程图") + private Integer diagramType; + + /** + * 绑定表单类型。 + */ + @Schema(description = "绑定表单类型") + private Integer bindFormType; + + /** + * 在线表单的页面Id。 + */ + @Schema(description = "在线表单的页面Id") + private Long pageId; + + /** + * 在线表单Id。 + */ + @Schema(description = "在线表单Id") + private Long defaultFormId; + + /** + * 在线表单的缺省路由名称。 + */ + @Schema(description = "在线表单的缺省路由名称") + private String defaultRouterName; + + /** + * 工单表编码字段的编码规则,如果为空则不计算工单编码。 + */ + @Schema(description = "工单表编码字段的编码规则") + private String encodedRule; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @Schema(description = "流程的自定义扩展数据") + private String extensionData; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * categoryId 的一对一关联数据对象,数据对应类型为FlowCategoryVo。 + */ + @Schema(description = "categoryId 的一对一关联数据对象") + private Map flowCategory; + + /** + * mainEntryPublishId 的一对一关联数据对象,数据对应类型为FlowEntryPublishVo。 + */ + @Schema(description = "mainEntryPublishId 的一对一关联数据对象") + private Map mainFlowEntryPublish; + + /** + * 关联的在线表单列表。 + */ + @Schema(description = "关联的在线表单列表") + private List> formList; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java new file mode 100644 index 00000000..8d7d104b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java @@ -0,0 +1,137 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 工作流通知消息Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流通知消息Vo对象") +@Data +public class FlowMessageVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long messageId; + + /** + * 消息类型。 + */ + @Schema(description = "消息类型") + private Integer messageType; + + /** + * 消息内容。 + */ + @Schema(description = "消息内容") + private String messageContent; + + /** + * 催办次数。 + */ + @Schema(description = "催办次数") + private Integer remindCount; + + /** + * 工单Id。 + */ + @Schema(description = "工单Id") + private Long workOrderId; + + /** + * 流程定义Id。 + */ + @Schema(description = "流程定义Id") + private String processDefinitionId; + + /** + * 流程定义标识。 + */ + @Schema(description = "流程定义标识") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 流程实例发起者。 + */ + @Schema(description = "流程实例发起者") + private String processInstanceInitiator; + + /** + * 流程任务Id。 + */ + @Schema(description = "流程任务Id") + private String taskId; + + /** + * 流程任务定义标识。 + */ + @Schema(description = "流程任务定义标识") + private String taskDefinitionKey; + + /** + * 流程任务名称。 + */ + @Schema(description = "流程任务名称") + private String taskName; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date taskStartTime; + + /** + * 业务数据快照。 + */ + @Schema(description = "业务数据快照") + private String businessDataShot; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 创建者显示名。 + */ + @Schema(description = "创建者显示名") + private String createUsername; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java new file mode 100644 index 00000000..c8328b34 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java @@ -0,0 +1,113 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * FlowTaskCommentVO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "FlowTaskCommentVO对象") +@Data +public class FlowTaskCommentVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long id; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 任务Id。 + */ + @Schema(description = "任务Id") + private String taskId; + + /** + * 任务标识。 + */ + @Schema(description = "任务标识") + private String taskKey; + + /** + * 任务名称。 + */ + @Schema(description = "任务名称") + private String taskName; + + /** + * 任务的执行Id。 + */ + @Schema(description = "任务的执行Id") + private String executionId; + + /** + * 会签任务的执行Id。 + */ + @Schema(description = "会签任务的执行Id") + private String multiInstanceExecId; + + /** + * 审批类型。 + */ + @Schema(description = "审批类型") + private String approvalType; + + /** + * 批注内容。 + */ + @Schema(description = "批注内容") + private String taskComment; + + /** + * 委托指定人,比如加签、转办等。 + */ + @Schema(description = "委托指定人,比如加签、转办等") + private String delegateAssignee; + + /** + * 自定义数据。开发者可自行扩展,推荐使用JSON格式数据。 + */ + @Schema(description = "自定义数据") + private String customBusinessData; + + /** + * 审批人头像。 + */ + @Schema(description = "审批人头像") + private String headImageUrl; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 创建者登录名。 + */ + @Schema(description = "创建者登录名") + private String createLoginName; + + /** + * 创建者显示名。 + */ + @Schema(description = "创建者显示名") + private String createUsername; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java new file mode 100644 index 00000000..35e4c367 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java @@ -0,0 +1,125 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程任务Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务Vo对象") +@Data +public class FlowTaskVo { + + /** + * 流程任务Id。 + */ + @Schema(description = "流程任务Id") + private String taskId; + + /** + * 流程任务名称。 + */ + @Schema(description = "流程任务名称") + private String taskName; + + /** + * 流程任务标识。 + */ + @Schema(description = "流程任务标识") + private String taskKey; + + /** + * 任务的表单信息。 + */ + @Schema(description = "任务的表单信息") + private String taskFormKey; + + /** + * 待办任务开始时间。 + */ + @Schema(description = "待办任务开始时间") + private Date taskStartTime; + + /** + * 流程Id。 + */ + @Schema(description = "流程Id") + private Long entryId; + + /** + * 流程定义Id。 + */ + @Schema(description = "流程定义Id") + private String processDefinitionId; + + /** + * 流程定义名称。 + */ + @Schema(description = "流程定义名称") + private String processDefinitionName; + + /** + * 流程定义标识。 + */ + @Schema(description = "流程定义标识") + private String processDefinitionKey; + + /** + * 流程定义版本。 + */ + @Schema(description = "流程定义版本") + private Integer processDefinitionVersion; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 流程实例发起人。 + */ + @Schema(description = "流程实例发起人") + private String processInstanceInitiator; + + /** + * 流程实例发起人显示名。 + */ + @Schema(description = "流程实例发起人显示名") + private String showName; + + /** + * 用户头像信息。 + */ + @Schema(description = "用户头像信息") + private String headImageUrl; + + /** + * 流程实例创建时间。 + */ + @Schema(description = "流程实例创建时间") + private Date processInstanceStartTime; + + /** + * 流程实例主表业务数据主键。 + */ + @Schema(description = "流程实例主表业务数据主键") + private String businessKey; + + /** + * 工单编码。 + */ + @Schema(description = "工单编码") + private String workOrderCode; + + /** + * 是否为草稿状态。 + */ + @Schema(description = "是否为草稿状态") + private Boolean isDraft; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java new file mode 100644 index 00000000..2ceca1fa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程任务的用户信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务的用户信息") +@Data +public class FlowUserInfoVo { + + /** + * 用户Id。 + */ + @Schema(description = "用户Id") + private Long userId; + + /** + * 用户部门Id。 + */ + @Schema(description = "用户部门Id") + private Long deptId; + + /** + * 登录用户名。 + */ + @Schema(description = "登录用户名") + private String loginName; + + /** + * 用户显示名称。 + */ + @Schema(description = "用户显示名称") + private String showName; + + /** + * 用户头像的Url。 + */ + @Schema(description = "用户头像的Url") + private String headImageUrl; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)") + private Integer userType; + + /** + * 用户状态(0: 正常 1: 锁定)。 + */ + @Schema(description = "用户状态(0: 正常 1: 锁定)") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + @Schema(description = "用户邮箱") + private String email; + + /** + * 用户手机。 + */ + @Schema(description = "用户手机") + private String mobile; + + /** + * 最后审批时间。 + */ + @Schema(description = "最后审批时间") + private Date lastApprovalTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java new file mode 100644 index 00000000..3122ed8f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java @@ -0,0 +1,158 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.alibaba.fastjson.JSONArray; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 工作流工单VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流工单Vo对象") +@Data +public class FlowWorkOrderVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long workOrderId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码") + private String appCode; + + /** + * 工单编码字段。 + */ + @Schema(description = "工单编码字段") + private String workOrderCode; + + /** + * 流程定义标识。 + */ + @Schema(description = "流程定义标识") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程引擎的定义Id。 + */ + @Schema(description = "流程引擎的定义Id") + private String processDefinitionId; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 在线表单的主表Id。 + */ + @Schema(description = "在线表单的主表Id") + private Long onlineTableId; + + /** + * 业务主键值。 + */ + @Schema(description = "业务主键值") + private String businessKey; + + /** + * 最近的审批状态。 + */ + @Schema(description = "最近的审批状态") + private Integer latestApprovalStatus; + + /** + * 流程状态。参考FlowTaskStatus常量值对象。 + */ + @Schema(description = "流程状态") + private Integer flowStatus; + + /** + * 提交用户登录名称。 + */ + @Schema(description = "提交用户登录名称") + private String submitUsername; + + /** + * 提交用户所在部门Id。 + */ + @Schema(description = "提交用户所在部门Id") + private Long deptId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * latestApprovalStatus 关联的字典数据。 + */ + @Schema(description = "latestApprovalStatus 常量字典关联数据") + private Map latestApprovalStatusDictMap; + + /** + * flowStatus 常量字典关联数据。 + */ + @Schema(description = "flowStatus 常量字典关联数据") + private Map flowStatusDictMap; + + /** + * 用户的显示名。 + */ + @Schema(description = "用户的显示名") + private String userShowName; + + /** + * FlowEntryPublish对象中的同名字段。 + */ + @Schema(description = "FlowEntryPublish对象中的同名字段") + private String initTaskInfo; + + /** + * 当前实例的运行时任务列表。 + * 正常情况下只有一个,在并行网关下可能存在多个。 + */ + @Schema(description = "实例的运行时任务列表") + private JSONArray runtimeTaskInfoList; + + /** + * 业务主表数据。 + */ + @Schema(description = "业务主表数据") + private Map masterData; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java new file mode 100644 index 00000000..2d4f981a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java @@ -0,0 +1,85 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +import java.util.List; + +/** + * 流程任务信息Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务信息Vo对象") +@Data +public class TaskInfoVo { + + /** + * 流程节点任务类型。具体值可参考FlowTaskType常量值。 + */ + @Schema(description = "流程节点任务类型") + private Integer taskType; + + /** + * 指定人。 + */ + @Schema(description = "指定人") + private String assignee; + + /** + * 任务标识。 + */ + @Schema(description = "任务标识") + private String taskKey; + + /** + * 是否分配给当前登录用户的标记。 + * 当该值为true时,登录用户启动流程时,就自动完成了第一个用户任务。 + */ + @Schema(description = "是否分配给当前登录用户的标记") + private Boolean assignedMe; + + /** + * 动态表单Id。 + */ + @Schema(description = "动态表单Id") + private Long formId; + + /** + * PC端静态表单路由。 + */ + @Schema(description = "PC端静态表单路由") + private String routerName; + + /** + * 移动端静态表单路由。 + */ + @Schema(description = "移动端静态表单路由") + private String mobileRouterName; + + /** + * 候选组类型。 + */ + @Schema(description = "候选组类型") + private String groupType; + + /** + * 只读标记。 + */ + @Schema(description = "只读标记") + private Boolean readOnly; + + /** + * 前端所需的操作列表。 + */ + @Schema(description = "前端所需的操作列表") + List operationList; + + /** + * 任务节点的自定义变量列表。 + */ + @Schema(description = "任务节点的自定义变量列表") + List variableList; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator new file mode 100644 index 00000000..eda90b8a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator @@ -0,0 +1 @@ +com.orangeforms.common.flow.config.CustomEngineConfigurator \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..8c6f8611 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.flow.config.FlowAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-log/pom.xml new file mode 100644 index 00000000..4f39b309 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-log + 1.0.0 + common-log + jar + + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java new file mode 100644 index 00000000..00bbe1f6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.log.annotation; + +import java.lang.annotation.*; + +/** + * 忽略接口应答数据记录日志的注解。该注解会被OperationLogAspect处理。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface IgnoreResponseLog { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java new file mode 100644 index 00000000..32f6b591 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.log.annotation; + +import com.orangeforms.common.log.model.constant.SysOperationLogType; + +import java.lang.annotation.*; + +/** + * 操作日志记录注解。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OperationLog { + + /** + * 描述。 + */ + String description() default ""; + + /** + * 操作类型。 + */ + int type() default SysOperationLogType.OTHER; + + /** + * 是否保存应答结果。 + * 对于类似导出和文件下载之类的接口,该参与应该设置为false。 + */ + boolean saveResponse() default true; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java new file mode 100644 index 00000000..b71c5df0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java @@ -0,0 +1,265 @@ +package com.orangeforms.common.log.aop; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.IpUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.log.annotation.IgnoreResponseLog; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.config.OperationLogProperties; +import com.orangeforms.common.log.model.SysOperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.log.service.SysOperationLogService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 操作日志记录处理AOP对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class OperationLogAspect { + + @Value("${spring.application.name}") + private String serviceName; + @Autowired + private SysOperationLogService operationLogService; + @Autowired + private OperationLogProperties properties; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 错误信息、请求参数和应答结果字符串的最大长度。 + */ + private static final int MAX_LENGTH = 2000; + + /** + * 所有controller方法。 + */ + @Pointcut("execution(public * com.orangeforms..controller..*(..))") + public void operationLogPointCut() { + // 空注释,避免sonar警告 + } + + @Around("operationLogPointCut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 计时。 + long start = System.currentTimeMillis(); + HttpServletRequest request = ContextUtil.getHttpRequest(); + HttpServletResponse response = ContextUtil.getHttpResponse(); + String traceId = this.getTraceId(request); + request.setAttribute(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + // 将流水号通过应答头返回给前端,便于问题精确定位。 + response.setHeader(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + TokenData tokenData = TokenData.takeFromRequest(); + // 为日志框架设定变量,使日志可以输出更多有价值的信息。 + if (tokenData != null) { + MDC.put("sessionId", tokenData.getSessionId()); + MDC.put("userId", tokenData.getUserId().toString()); + } + String[] parameterNames = this.getParameterNames(joinPoint); + Object[] args = joinPoint.getArgs(); + JSONObject jsonArgs = new JSONObject(); + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (this.isNormalArgs(arg)) { + String parameterName = parameterNames[i]; + jsonArgs.put(parameterName, arg); + } + } + String params = jsonArgs.toJSONString(); + SysOperationLog operationLog = null; + OperationLog operationLogAnnotation = null; + boolean saveOperationLog = properties.isEnabled(); + if (saveOperationLog) { + operationLogAnnotation = getMethodAnnotation(joinPoint, OperationLog.class); + saveOperationLog = (operationLogAnnotation != null); + } + if (saveOperationLog) { + operationLog = this.buildSysOperationLog(operationLogAnnotation, joinPoint, params, traceId, tokenData); + } + Object result; + log.info("开始请求,url={}, reqData={}", request.getRequestURI(), params); + try { + // 调用原来的方法 + result = joinPoint.proceed(); + String respData = result == null ? "null" : JSON.toJSONString(result); + Long elapse = System.currentTimeMillis() - start; + if (saveOperationLog) { + this.operationLogPostProcess(operationLogAnnotation, respData, operationLog, result); + } + if (elapse > properties.getSlowLogMs()) { + log.warn("耗时较长的请求完成警告, url={},elapse={}ms reqData={} respData={}", + request.getRequestURI(), elapse, params, respData); + } + if (this.getMethodAnnotation(joinPoint, IgnoreResponseLog.class) == null) { + log.info("请求完成, url={},elapse={}ms, respData={}", request.getRequestURI(), elapse, respData); + } + } catch (Exception e) { + if (saveOperationLog) { + operationLog.setSuccess(false); + operationLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, MAX_LENGTH)); + } + log.error("请求报错,url={}, reqData={}, error={}", request.getRequestURI(), params, e.getMessage()); + throw e; + } finally { + if (saveOperationLog) { + operationLog.setElapse(System.currentTimeMillis() - start); + operationLogService.saveNewAsync(operationLog); + } + MDC.remove(ApplicationConstant.HTTP_HEADER_TRACE_ID); + if (tokenData != null) { + MDC.remove("sessionId"); + MDC.remove("userId"); + } + } + return result; + } + + private SysOperationLog buildSysOperationLog( + OperationLog operationLogAnnotation, + ProceedingJoinPoint joinPoint, + String params, + String traceId, + TokenData tokenData) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + SysOperationLog operationLog = new SysOperationLog(); + operationLog.setLogId(idGenerator.nextLongId()); + operationLog.setTraceId(traceId); + operationLog.setDescription(operationLogAnnotation.description()); + operationLog.setOperationType(operationLogAnnotation.type()); + operationLog.setServiceName(this.serviceName); + operationLog.setApiClass(joinPoint.getTarget().getClass().getName()); + operationLog.setApiMethod(operationLog.getApiClass() + "." + joinPoint.getSignature().getName()); + operationLog.setRequestMethod(request.getMethod()); + operationLog.setRequestUrl(request.getRequestURI()); + if (tokenData != null) { + operationLog.setRequestIp(tokenData.getLoginIp()); + } else { + operationLog.setRequestIp(IpUtil.getRemoteIpAddress(request)); + } + operationLog.setOperationTime(new Date()); + if (params != null) { + if (params.length() <= MAX_LENGTH) { + operationLog.setRequestArguments(params); + } else { + operationLog.setRequestArguments(StringUtils.substring(params, 0, MAX_LENGTH)); + } + } + if (tokenData != null) { + // 对于非多租户系统,该值为空可以忽略。 + operationLog.setTenantId(tokenData.getTenantId()); + operationLog.setSessionId(tokenData.getSessionId()); + operationLog.setOperatorId(tokenData.getUserId()); + operationLog.setOperatorName(tokenData.getLoginName()); + } + return operationLog; + } + + private void operationLogPostProcess( + OperationLog operationLogAnnotation, String respData, SysOperationLog operationLog, Object result) { + if (operationLogAnnotation.saveResponse()) { + if (respData.length() <= MAX_LENGTH) { + operationLog.setResponseResult(respData); + } else { + operationLog.setResponseResult(StringUtils.substring(respData, 0, MAX_LENGTH)); + } + } + // 处理大部分返回ResponseResult的接口。 + if (!(result instanceof ResponseResult)) { + if (ContextUtil.hasRequestContext()) { + operationLog.setSuccess(ContextUtil.getHttpResponse().getStatus() == HttpServletResponse.SC_OK); + } + return; + } + ResponseResult responseResult = (ResponseResult) result; + operationLog.setSuccess(responseResult.isSuccess()); + if (!responseResult.isSuccess()) { + operationLog.setErrorMsg(responseResult.getErrorMessage()); + } + if (operationLog.getOperationType().equals(SysOperationLogType.LOGIN)) { + // 对于登录操作,由于在调用登录方法之前,没有可用的TokenData。 + // 因此如果登录成功,可再次通过TokenData.takeFromRequest()获取TokenData。 + if (BooleanUtil.isTrue(operationLog.getSuccess())) { + // 这里为了保证LoginController.doLogin方法,一定将TokenData存入Request.Attribute之中, + // 我们将不做空值判断,一旦出错,开发者可在调试时立刻发现异常,并根据这里的注释进行修复。 + TokenData tokenData = TokenData.takeFromRequest(); + // 对于非多租户系统,为了保证代码一致性,仍可保留对tenantId的赋值代码。 + operationLog.setTenantId(tokenData.getTenantId()); + operationLog.setSessionId(tokenData.getSessionId()); + operationLog.setOperatorId(tokenData.getUserId()); + operationLog.setOperatorName(tokenData.getLoginName()); + } else { + HttpServletRequest request = ContextUtil.getHttpRequest(); + // 登录操作需要特殊处理,无论是登录成功还是失败,都要记录operator_name字段。 + operationLog.setOperatorName(request.getParameter("loginName")); + } + } + } + + private String[] getParameterNames(ProceedingJoinPoint joinPoint) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + return methodSignature.getParameterNames(); + } + + private T getMethodAnnotation(JoinPoint joinPoint, Class annotationClazz) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method method = methodSignature.getMethod(); + return method.getAnnotation(annotationClazz); + } + + private String getTraceId(HttpServletRequest request) { + // 获取请求流水号。 + // 对于微服务系统,为了保证traceId在全调用链的唯一性,因此在网关的过滤器中创建了该值。 + String traceId = request.getHeader(ApplicationConstant.HTTP_HEADER_TRACE_ID); + if (StringUtils.isBlank(traceId)) { + traceId = MyCommonUtil.generateUuid(); + } + return traceId; + } + + private boolean isNormalArgs(Object o) { + if (o instanceof List) { + List list = (List) o; + if (CollUtil.isNotEmpty(list)) { + return !(list.get(0) instanceof MultipartFile); + } + } + return !(o instanceof HttpServletRequest) + && !(o instanceof HttpServletResponse) + && !(o instanceof MultipartFile); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java new file mode 100644 index 00000000..54444158 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.log.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-log模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({OperationLogProperties.class}) +public class CommonLogAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java new file mode 100644 index 00000000..cd8c95d6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.log.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 操作日志的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-log.operation-log") +public class OperationLogProperties { + + /** + * 是否采集操作日志。 + */ + private boolean enabled = true; + /** + * 接口调用的毫秒数大于该值后,将输出慢日志警告。 + */ + private long slowLogMs = 50000; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java new file mode 100644 index 00000000..63e5ec4c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.log.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.log.model.SysOperationLog; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 系统操作日志对应的数据访问对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysOperationLogMapper extends BaseDaoMapper { + + /** + * 批量插入。 + * + * @param operationLogList 操作日志列表。 + */ + void insertList(List operationLogList); + + /** + * 根据过滤条件和排序规则,查询操作日志。 + * + * @param sysOperationLogFilter 操作日志的过滤对象。 + * @param orderBy 排序规则。 + * @return 查询列表。 + */ + List getSysOperationLogList( + @Param("sysOperationLogFilter") SysOperationLog sysOperationLogFilter, + @Param("orderBy") String orderBy); +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml new file mode 100644 index 00000000..f29559f1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_sys_operation_log.operation_type = #{sysOperationLogFilter.operationType} + + + + AND zz_sys_operation_log.request_url LIKE #{safeRequestUrl} + + + AND zz_sys_operation_log.trace_id = #{sysOperationLogFilter.traceId} + + + AND zz_sys_operation_log.success = #{sysOperationLogFilter.success} + + + + AND zz_sys_operation_log.operator_name LIKE #{safeOperatorName} + + + AND zz_sys_operation_log.elapse >= #{sysOperationLogFilter.elapseMin} + + + AND zz_sys_operation_log.elapse <= #{sysOperationLogFilter.elapseMax} + + + AND zz_sys_operation_log.operation_time >= #{sysOperationLogFilter.operationTimeStart} + + + AND zz_sys_operation_log.operation_time <= #{sysOperationLogFilter.operationTimeEnd} + + + + + + INSERT INTO zz_sys_operation_log VALUES + + (#{item.logId}, + #{item.description}, + #{item.operationType}, + #{item.serviceName}, + #{item.apiClass}, + #{item.apiMethod}, + #{item.sessionId}, + #{item.traceId}, + #{item.elapse}, + #{item.requestMethod}, + #{item.requestUrl}, + #{item.requestArguments}, + #{item.responseResult}, + #{item.requestIp}, + #{item.success}, + #{item.errorMsg}, + #{item.tenantId}, + #{item.operatorId}, + #{item.operatorName}, + #{item.operationTime}) + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java new file mode 100644 index 00000000..994f51f0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.log.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 操作日志记录表 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "操作日志Dto") +@Data +public class SysOperationLogDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long logId; + + /** + * 操作类型。 + * 常量值定义可参考SysOperationLogType对象。 + */ + @Schema(description = "操作类型") + private Integer operationType; + + /** + * 每次请求的Id。 + * 对于微服务之间的调用,在同一个请求的调用链中,该值是相同的。 + */ + @Schema(description = "每次请求的Id") + private String traceId; + + /** + * HTTP 请求地址。 + */ + @Schema(description = "HTTP 请求地址") + private String requestUrl; + + /** + * 应答状态。 + */ + @Schema(description = "应答状态") + private Boolean success; + + /** + * 操作员名称。 + */ + @Schema(description = "操作员名称") + private String operatorName; + + /** + * 调用时长最小值。 + */ + @Schema(description = "调用时长最小值") + private Long elapseMin; + + /** + * 调用时长最大值。 + */ + @Schema(description = "调用时长最大值") + private Long elapseMax; + + /** + * 操作开始时间。 + */ + @Schema(description = "操作开始时间") + private String operationTimeStart; + + /** + * 操作开始时间。 + */ + @Schema(description = "操作开始时间") + private String operationTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java new file mode 100644 index 00000000..82e9951d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java @@ -0,0 +1,170 @@ +package com.orangeforms.common.log.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.TenantFilterColumn; +import lombok.Data; + +import java.util.Date; + +/** + * 操作日志记录表 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table("zz_sys_operation_log") +public class SysOperationLog { + + /** + * 主键Id。 + */ + @Id(value = "log_id") + private Long logId; + + /** + * 日志描述。 + */ + @Column(value = "description") + private String description; + + /** + * 操作类型。 + * 常量值定义可参考SysOperationLogType对象。 + */ + @Column(value = "operation_type") + private Integer operationType; + + /** + * 接口所在服务名称。 + * 通常为spring.application.name配置项的值。 + */ + @Column(value = "service_name") + private String serviceName; + + /** + * 调用的controller全类名。 + * 之所以为独立字段,是为了便于查询和统计接口的调用频度。 + */ + @Column(value = "api_class") + private String apiClass; + + /** + * 调用的controller中的方法。 + * 格式为:接口类名 + "." + 方法名。 + */ + @Column(value = "api_method") + private String apiMethod; + + /** + * 用户会话sessionId。 + * 主要是为了便于统计,以及跟踪查询定位问题。 + */ + @Column(value = "session_id") + private String sessionId; + + /** + * 每次请求的Id。 + * 对于微服务之间的调用,在同一个请求的调用链中,该值是相同的。 + */ + @Column(value = "trace_id") + private String traceId; + + /** + * 调用时长。 + */ + @Column(value = "elapse") + private Long elapse; + + /** + * HTTP 请求方法,如GET。 + */ + @Column(value = "request_method") + private String requestMethod; + + /** + * HTTP 请求地址。 + */ + @Column(value = "request_url") + private String requestUrl; + + /** + * controller接口参数。 + */ + @Column(value = "request_arguments") + private String requestArguments; + + /** + * controller应答结果。 + */ + @Column(value = "response_result") + private String responseResult; + + /** + * 请求IP。 + */ + @Column(value = "request_ip") + private String requestIp; + + /** + * 应答状态。 + */ + @Column(value = "success") + private Boolean success; + + /** + * 错误信息。 + */ + @Column(value = "error_msg") + private String errorMsg; + + /** + * 租户Id。 + * 仅用于多租户系统,是便于进行对租户的操作查询和统计分析。 + */ + @TenantFilterColumn + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 操作员Id。 + */ + @Column(value = "operator_id") + private Long operatorId; + + /** + * 操作员名称。 + */ + @Column(value = "operator_name") + private String operatorName; + + /** + * 操作时间。 + */ + @Column(value = "operation_time") + private Date operationTime; + + /** + * 调用时长最小值。 + */ + @Column(ignore = true) + private Long elapseMin; + + /** + * 调用时长最大值。 + */ + @Column(ignore = true) + private Long elapseMax; + + /** + * 操作开始时间。 + */ + @Column(ignore = true) + private String operationTimeStart; + + /** + * 操作结束时间。 + */ + @Column(ignore = true) + private String operationTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java new file mode 100644 index 00000000..ec3edaf5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java @@ -0,0 +1,145 @@ +package com.orangeforms.common.log.model.constant; + +/** + * 操作日志类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysOperationLogType { + + /** + * 其他。 + */ + public static final int OTHER = -1; + /** + * 登录。 + */ + public static final int LOGIN = 0; + /** + * 登录移动端。 + */ + public static final int LOGIN_MOBILE = 1; + /** + * 登出。 + */ + public static final int LOGOUT = 5; + /** + * 登出移动端。 + */ + public static final int LOGOUT_MOBILE = 6; + /** + * 新增。 + */ + public static final int ADD = 10; + /** + * 修改。 + */ + public static final int UPDATE = 15; + /** + * 删除。 + */ + public static final int DELETE = 20; + /** + * 批量删除。 + */ + public static final int DELETE_BATCH = 21; + /** + * 新增多对多关联。 + */ + public static final int ADD_M2M = 25; + /** + * 移除多对多关联。 + */ + public static final int DELETE_M2M = 30; + /** + * 批量移除多对多关联。 + */ + public static final int DELETE_M2M_BATCH = 31; + /** + * 查询。 + */ + public static final int LIST = 35; + /** + * 分组查询。 + */ + public static final int LIST_WITH_GROUP = 40; + /** + * 导出。 + */ + public static final int EXPORT = 45; + /** + * 导入。 + */ + public static final int IMPORT = 46; + /** + * 上传。 + */ + public static final int UPLOAD = 50; + /** + * 下载。 + */ + public static final int DOWNLOAD = 55; + /** + * 重置缓存。 + */ + public static final int RELOAD_CACHE = 60; + /** + * 发布。 + */ + public static final int PUBLISH = 65; + /** + * 取消发布。 + */ + public static final int UNPUBLISH = 70; + /** + * 暂停。 + */ + public static final int SUSPEND = 75; + /** + * 恢复。 + */ + public static final int RESUME = 80; + /** + * 启动流程。 + */ + public static final int START_FLOW = 100; + /** + * 停止流程。 + */ + public static final int STOP_FLOW = 105; + /** + * 删除流程。 + */ + public static final int DELETE_FLOW = 110; + /** + * 取消流程。 + */ + public static final int CANCEL_FLOW = 115; + /** + * 提交任务。 + */ + public static final int SUBMIT_TASK = 120; + /** + * 催办任务。 + */ + public static final int REMIND_TASK = 125; + /** + * 干预任务。 + */ + public static final int INTERVENE_FLOW = 126; + /** + * 修复流程的业务数据。 + */ + public static final int FIX_FLOW_BUSINESS_DATA = 127; + /** + * 流程复活。 + */ + public static final int REVIVE_FLOW = 128; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysOperationLogType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java new file mode 100644 index 00000000..18c1b087 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java @@ -0,0 +1,45 @@ +package com.orangeforms.common.log.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.log.model.SysOperationLog; + +import java.util.List; + +/** + * 操作日志服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysOperationLogService extends IBaseService { + + /** + * 异步的插入一条新操作日志。 + * + * @param operationLog 操作日志对象。 + */ + void saveNewAsync(SysOperationLog operationLog); + + /** + * 插入一条新操作日志。 + * + * @param operationLog 操作日志对象。 + */ + void saveNew(SysOperationLog operationLog); + + /** + * 批量插入。 + * + * @param sysOperationLogList 操作日志列表。 + */ + void batchSave(List sysOperationLogList); + + /** + * 根据过滤条件和排序规则,查询操作日志。 + * + * @param filter 操作日志的过滤对象。 + * @param orderBy 排序规则。 + * @return 查询列表。 + */ + List getSysOperationLogList(SysOperationLog filter, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java new file mode 100644 index 00000000..3935df68 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java @@ -0,0 +1,84 @@ +package com.orangeforms.common.log.service.impl; + +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.log.dao.SysOperationLogMapper; +import com.orangeforms.common.log.model.SysOperationLog; +import com.orangeforms.common.log.service.SysOperationLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 操作日志服务实现类。 + * 这里需要重点解释下MyDataSource注解。在单数据源服务中,由于没有DataSourceAspect的切面类,所以该注解不会 + * 有任何作用和影响。然而在多数据源情况下,由于每个服务都有自己的DataSourceType常量对象,表示不同的数据源。 + * 而common-log在公用模块中,不能去依赖业务服务,因此这里给出了一个固定值。我们在业务的DataSourceType中,也要 + * 使用该值ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE,去关联操作日志所需的数据源配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@MyDataSource(ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE) +@Service +public class SysOperationLogServiceImpl extends BaseService implements SysOperationLogService { + + @Autowired + private SysOperationLogMapper sysOperationLogMapper; + + @Override + protected BaseDaoMapper mapper() { + return sysOperationLogMapper; + } + + /** + * 异步插入一条新操作日志。通常用于在橙单中创建的单体工程服务。 + * + * @param operationLog 操作日志对象。 + */ + @Async + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAsync(SysOperationLog operationLog) { + sysOperationLogMapper.insert(operationLog); + } + + /** + * 插入一条新操作日志。 + * + * @param operationLog 操作日志对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNew(SysOperationLog operationLog) { + sysOperationLogMapper.insert(operationLog); + } + + /** + * 批量插入。通常用于在橙单中创建的微服务工程服务。 + * + * @param sysOperationLogList 操作日志列表。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void batchSave(List sysOperationLogList) { + sysOperationLogMapper.insertList(sysOperationLogList); + } + + /** + * 根据过滤条件和排序规则,查询操作日志。 + * + * @param filter 操作日志的过滤对象。 + * @param orderBy 排序规则。 + * @return 查询列表。 + */ + @Override + public List getSysOperationLogList(SysOperationLog filter, String orderBy) { + return sysOperationLogMapper.getSysOperationLogList(filter, orderBy); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java new file mode 100644 index 00000000..983ea9ed --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java @@ -0,0 +1,144 @@ +package com.orangeforms.common.log.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 操作日志记录表 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "操作日志VO") +@Data +public class SysOperationLogVo { + + /** + * 操作日志主键Id。 + */ + @Schema(description = "操作日志主键Id") + private Long logId; + + /** + * 日志描述。 + */ + @Schema(description = "日志描述") + private String description; + + /** + * 操作类型。 + * 常量值定义可参考SysOperationLogType对象。 + */ + @Schema(description = "操作类型") + private Integer operationType; + + /** + * 接口所在服务名称。 + * 通常为spring.application.name配置项的值。 + */ + @Schema(description = "接口所在服务名称") + private String serviceName; + + /** + * 调用的controller全类名。 + * 之所以为独立字段,是为了便于查询和统计接口的调用频度。 + */ + @Schema(description = "调用的controller全类名") + private String apiClass; + + /** + * 调用的controller中的方法。 + * 格式为:接口类名 + "." + 方法名。 + */ + @Schema(description = "调用的controller中的方法") + private String apiMethod; + + /** + * 用户会话sessionId。 + * 主要是为了便于统计,以及跟踪查询定位问题。 + */ + @Schema(description = "用户会话sessionId") + private String sessionId; + + /** + * 每次请求的Id。 + * 对于微服务之间的调用,在同一个请求的调用链中,该值是相同的。 + */ + @Schema(description = "每次请求的Id") + private String traceId; + + /** + * 调用时长。 + */ + @Schema(description = "调用时长") + private Long elapse; + + /** + * HTTP 请求方法,如GET。 + */ + @Schema(description = "HTTP 请求方法") + private String requestMethod; + + /** + * HTTP 请求地址。 + */ + @Schema(description = "HTTP 请求地址") + private String requestUrl; + + /** + * controller接口参数。 + */ + @Schema(description = "controller接口参数") + private String requestArguments; + + /** + * controller应答结果。 + */ + @Schema(description = "controller应答结果") + private String responseResult; + + /** + * 请求IP。 + */ + @Schema(description = "请求IP") + private String requestIp; + + /** + * 应答状态。 + */ + @Schema(description = "应答状态") + private Boolean success; + + /** + * 错误信息。 + */ + @Schema(description = "错误信息") + private String errorMsg; + + /** + * 租户Id。 + * 仅用于多租户系统,是便于进行对租户的操作查询和统计分析。 + */ + @Schema(description = "租户Id") + private Long tenantId; + + /** + * 操作员Id。 + */ + @Schema(description = "操作员Id") + private Long operatorId; + + /** + * 操作员名称。 + */ + @Schema(description = "操作员名称") + private String operatorName; + + /** + * 操作时间。 + */ + @Schema(description = "操作时间") + private Date operationTime; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..dff1b36f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.log.config.CommonLogAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-minio/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-minio/pom.xml new file mode 100644 index 00000000..178b8c8e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-minio/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-minio + 1.0.0 + common-minio + jar + + + + io.minio + minio + ${minio.version} + + + com.orangeforms + common-core + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java new file mode 100644 index 00000000..d89019ff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java @@ -0,0 +1,56 @@ +package com.orangeforms.common.minio.config; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.minio.wrapper.MinioTemplate; +import io.minio.BucketExistsArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * common-minio模块的自动配置引导类。仅当配置项minio.enabled为true的时候加载。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties(MinioProperties.class) +@ConditionalOnProperty(prefix = "minio", name = "enabled") +public class MinioAutoConfiguration { + + /** + * 将minio原生的客户端类封装成bean对象,便于集成,同时也可以灵活使用客户端的所有功能。 + * + * @param p 属性配置对象。 + * @return minio的原生客户端对象。 + */ + @Bean + @ConditionalOnMissingBean + public MinioClient minioClient(MinioProperties p) { + try { + MinioClient client = MinioClient.builder() + .endpoint(p.getEndpoint()).credentials(p.getAccessKey(), p.getSecretKey()).build(); + if (!client.bucketExists(BucketExistsArgs.builder().bucket(p.getBucketName()).build())) { + client.makeBucket(MakeBucketArgs.builder().bucket(p.getBucketName()).build()); + } + return client; + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 封装的minio模板类。 + * + * @param p 属性配置对象。 + * @param c minio的原生客户端bean对象。 + * @return minio模板的bean对象。 + */ + @Bean + @ConditionalOnMissingBean + public MinioTemplate minioTemplate(MinioProperties p, MinioClient c) { + return new MinioTemplate(p, c); + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java new file mode 100644 index 00000000..ecdf253d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java @@ -0,0 +1,32 @@ +package com.orangeforms.common.minio.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common-minio模块的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "minio") +public class MinioProperties { + + /** + * 访问入口地址。 + */ + private String endpoint; + /** + * 访问安全的key。 + */ + private String accessKey; + /** + * 访问安全的密钥。 + */ + private String secretKey; + /** + * 缺省桶名称。 + */ + private String bucketName; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java new file mode 100644 index 00000000..9c2c71a7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java @@ -0,0 +1,115 @@ +package com.orangeforms.common.minio.util; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.minio.wrapper.MinioTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; + +/** + * 基于Minio上传和下载文件操作的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +@ConditionalOnProperty(prefix = "minio", name = "enabled") +public class MinioUpDownloader extends BaseUpDownloader { + + @Autowired + private MinioTemplate minioTemplate; + @Autowired + private UpDownloaderFactory factory; + + @PostConstruct + public void doRegister() { + factory.registerUpDownloader(UploadStoreTypeEnum.MINIO_SYSTEM, this); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + String uploadPath = super.makeFullPath(null, modelName, fieldName, asImage); + return this.doUploadInternally(serviceContextPath, uploadPath, asImage, uploadFile); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String uriPath, + MultipartFile uploadFile) throws IOException { + String uploadPath = super.makeFullPath(null, uriPath); + return this.doUploadInternally(serviceContextPath, uploadPath, false, uploadFile); + } + + @Override + public void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) throws IOException { + String uploadPath = this.makeFullPath(null, modelName, fieldName, asImage); + String fullFileanme = uploadPath + "/" + fileName; + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException { + StringBuilder pathBuilder = new StringBuilder(128); + if (StrUtil.isNotBlank(uriPath)) { + pathBuilder.append(uriPath); + } + pathBuilder.append("/"); + String fullFileanme = pathBuilder.append(fileName).toString(); + this.downloadInternal(fullFileanme, fileName, response); + } + + private UploadResponseInfo doUploadInternally( + String serviceContextPath, + String uploadPath, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = super.verifyUploadArgument(asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + return responseInfo; + } + responseInfo.setUploadPath(uploadPath); + super.fillUploadResponseInfo(responseInfo, serviceContextPath, uploadFile.getOriginalFilename()); + minioTemplate.putObject(uploadPath + "/" + responseInfo.getFilename(), uploadFile.getInputStream()); + return responseInfo; + } + + private void downloadInternal(String fullFileanme, String fileName, HttpServletResponse response) throws IOException { + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + InputStream in = minioTemplate.getStream(fullFileanme); + IoUtil.copy(in, response.getOutputStream()); + in.close(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java new file mode 100644 index 00000000..dc29310f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java @@ -0,0 +1,199 @@ +package com.orangeforms.common.minio.wrapper; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.minio.config.MinioProperties; +import io.minio.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 封装的minio客户端模板类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MinioTemplate { + + private static final String TMP_DIR = System.getProperty("java.io.tmpdir") + File.separator; + private final MinioProperties properties; + private final MinioClient client; + + public MinioTemplate(MinioProperties properties, MinioClient client) { + super(); + this.properties = properties; + this.client = client; + } + + /** + * 判断bucket是否存在。 + * + * @param bucketName 桶名称。 + * @return 存在返回true,否则false。 + */ + public boolean bucketExists(String bucketName) { + try { + return client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 创建桶。 + * + * @param bucketName 桶名称。 + */ + public void makeBucket(String bucketName) { + try { + if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { + client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 存放对象。 + * + * @param bucketName 桶名称。 + * @param objectName 对象名称。 + * @param filename 本地上传的文件名称。 + */ + public void putObject(String bucketName, String objectName, String filename) { + try { + this.putObject(bucketName, objectName, new FileInputStream(filename)); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 存放对象。桶名称为配置中的桶名称。 + * + * @param objectName 对象名称。 + * @param filename 本地上传的文件名称。 + */ + public void putObject(String objectName, String filename) { + try { + this.putObject(properties.getBucketName(), objectName, filename); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 读取输入流并存放。 + * + * @param bucketName 桶名称。 + * @param objectName 对象名称。 + * @param stream 读取后上传的文件流。 + */ + public void putObject(String bucketName, String objectName, InputStream stream) { + try { + client.putObject(PutObjectArgs.builder() + .bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } finally { + try { + stream.close(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + } + + /** + * 读取输入流并存放。 + * + * @param objectName 对象名称。 + * @param stream 读取后上传的文件流。 + */ + public void putObject(String objectName, InputStream stream) { + this.putObject(properties.getBucketName(), objectName, stream); + } + + /** + * 移除对象。 + * + * @param bucketName 桶名称。 + * @param objectName 对象名称。 + */ + public void removeObject(String bucketName, String objectName) { + try { + client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 移除对象。桶名称为配置中的桶名称。 + * + * @param objectName 对象名称。 + */ + public void removeObject(String objectName) { + this.removeObject(properties.getBucketName(), objectName); + } + + /** + * 获取文件输入流。 + * + * @param bucket 桶名称。 + * @param objectName 对象名称。 + * @return 文件的输入流。 + */ + public InputStream getStream(String bucket, String objectName) { + try { + return client.getObject(GetObjectArgs.builder().bucket(bucket).object(objectName).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 获取文件输入流。 + * + * @param objectName 对象名称。 + * @return 文件的输入流。 + */ + public InputStream getStream(String objectName) { + return this.getStream(properties.getBucketName(), objectName); + } + + /** + * 获取存储的文件对象。 + * + * @param bucket 桶名称。 + * @param objectName 对象名称。 + * @return 读取后存储到文件的文件对象。 + */ + public File getFile(String bucket, String objectName) throws IOException { + InputStream in = getStream(bucket, objectName); + File dir = new File(TMP_DIR); + if (!dir.exists() || dir.isFile()) { + dir.mkdirs(); + } + File file = new File(TMP_DIR + objectName); + FileUtils.copyInputStreamToFile(in, file); + in.close(); + return file; + } + + /** + * 获取存储的文件对象。桶名称为配置中的桶名称。 + * + * @param objectName 对象名称。 + * @return 读取后存储到文件的文件对象。 + */ + public File getFile(String objectName) throws IOException { + return this.getFile(properties.getBucketName(), objectName); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..a7ba3af4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.minio.config.MinioAutoConfiguration \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/pom.xml new file mode 100644 index 00000000..c653f38f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/pom.xml @@ -0,0 +1,64 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-online + 1.0.0 + common-online + jar + + + + com.orangeforms + common-satoken + 1.0.0 + + + com.orangeforms + common-dbutil + 1.0.0 + + + com.orangeforms + common-dict + 1.0.0 + + + com.orangeforms + common-datafilter + 1.0.0 + + + com.orangeforms + common-redis + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-log + 1.0.0 + + + com.orangeforms + common-minio + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java new file mode 100644 index 00000000..2f18a739 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-online模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({OnlineProperties.class}) +public class OnlineAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java new file mode 100644 index 00000000..17308333 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.online.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + * 在线表单的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-online") +public class OnlineProperties { + + /** + * 脱敏字段的掩码。只能为单个字符。 + */ + private String maskChar = "*"; + /** + * 在调用render接口的时候,是否打开一级缓存加速页面渲染数据的获取。 + */ + private Boolean enableRenderCache = true; + /** + * 业务表和在线表单内置表是否跨库。 + */ + private Boolean enabledMultiDatabaseWrite = true; + /** + * 仅以该前缀开头的数据表才会成为动态表单的候选数据表,如: zz_。如果为空,则所有表均可被选。 + */ + private String tablePrefix; + /** + * 在线表单业务操作的URL前缀。 + */ + private String urlPrefix; + /** + * 在线表单打印接口的路径 + */ + private String printUrlPath; + /** + * 上传文件的根路径。 + */ + private String uploadFileBaseDir; + /** + * 1: minio 2: aliyun-oss 3: qcloud-cos。 + * 0是本地系统,不推荐使用。 + */ + private Integer distributeStoreType; + /** + * 在线表单查看权限的URL列表。 + */ + private List viewUrlList; + /** + * 在线表单编辑权限的URL列表。 + */ + private List editUrlList; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java new file mode 100644 index 00000000..52c169db --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java @@ -0,0 +1,517 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.MaskFieldTypeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineColumnDto; +import com.orangeforms.common.online.dto.OnlineColumnRuleDto; +import com.orangeforms.common.online.dto.OnlineRuleDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineColumnRuleVo; +import com.orangeforms.common.online.vo.OnlineColumnVo; +import com.orangeforms.common.online.vo.OnlineRuleVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 在线表单字段数据接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单字段数据接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineColumn") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineColumnController { + + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineRuleService onlineRuleService; + @Autowired + private OnlineDictService onlineDictService; + + /** + * 根据数据库表字段信息,在指定在线表中添加在线表字段对象。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 数据库表名称。 + * @param columnName 数据库表字段名。 + * @param tableId 目的表Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody Long dblinkId, + @MyRequestBody String tableName, + @MyRequestBody String columnName, + @MyRequestBody Long tableId) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMessage; + SqlTableColumn sqlTableColumn = onlineDblinkService.getDblinkTableColumn(dblink, tableName, columnName); + if (sqlTableColumn == null) { + errorMessage = "数据验证失败,指定的数据表字段不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult verifyResult = this.doVerifyTable(tableId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + onlineColumnService.saveNewList(CollUtil.newLinkedList(sqlTableColumn), tableId); + return ResponseResult.success(); + } + + /** + * 更新字段数据数据。 + * + * @param onlineColumnDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineColumnDto onlineColumnDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineColumnDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineColumn onlineColumn = MyModelUtil.copyTo(onlineColumnDto, OnlineColumn.class); + OnlineColumn originalOnlineColumn = onlineColumnService.getById(onlineColumn.getColumnId()); + if (originalOnlineColumn == null) { + errorMessage = "数据验证失败,当前在线表字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyColumnResult = this.doVerifyColumn(onlineColumn, originalOnlineColumn); + if (!verifyColumnResult.isSuccess()) { + return ResponseResult.errorFrom(verifyColumnResult); + } + ResponseResult verifyResult = this.doVerifyTable(originalOnlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineColumnService.update(onlineColumn, originalOnlineColumn)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除字段数据数据。 + * + * @param columnId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long columnId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(columnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineColumn originalOnlineColumn = onlineColumnService.getById(columnId); + if (originalOnlineColumn == null) { + errorMessage = "数据验证失败,当前在线表字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerifyTable(originalOnlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineVirtualColumn virtualColumnFilter = new OnlineVirtualColumn(); + virtualColumnFilter.setAggregationColumnId(columnId); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnList(virtualColumnFilter, null); + if (CollUtil.isNotEmpty(virtualColumnList)) { + OnlineVirtualColumn virtualColumn = virtualColumnList.get(0); + errorMessage = "数据验证失败,数据源关联正在被虚拟字段 [" + virtualColumn.getColumnPrompt() + "] 使用,不能被删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineColumnService.remove(originalOnlineColumn.getTableId(), columnId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的字段数据列表。 + * + * @param onlineColumnDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineColumnDto onlineColumnDtoFilter, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineColumn onlineColumnFilter = MyModelUtil.copyTo(onlineColumnDtoFilter, OnlineColumn.class); + List onlineColumnList = + onlineColumnService.getOnlineColumnListWithRelation(onlineColumnFilter); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineColumnList, OnlineColumnVo.class)); + } + + /** + * 查看指定字段数据对象详情。 + * + * @param columnId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long columnId) { + if (MyCommonUtil.existBlankArgument(columnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineColumn onlineColumn = onlineColumnService.getByIdWithRelation(columnId, MyRelationParam.full()); + if (onlineColumn == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineColumn, OnlineColumnVo.class); + } + + /** + * 将数据库中的表字段信息刷新到已经导入的在线表字段信息。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 数据库表名称。 + * @param columnName 数据库表字段名。 + * @param columnId 被刷新的在线字段Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/refresh") + public ResponseResult refresh( + @MyRequestBody Long dblinkId, + @MyRequestBody String tableName, + @MyRequestBody String columnName, + @MyRequestBody Long columnId) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMsg; + SqlTableColumn sqlTableColumn = onlineDblinkService.getDblinkTableColumn(dblink, tableName, columnName); + if (sqlTableColumn == null) { + errorMsg = "数据验证失败,指定的数据表字段不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMsg); + } + OnlineColumn onlineColumn = onlineColumnService.getById(columnId); + if (onlineColumn == null) { + errorMsg = "数据验证失败,指定的在线表字段Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMsg); + } + ResponseResult verifyResult = this.doVerifyTable(onlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + onlineColumnService.refresh(sqlTableColumn, onlineColumn); + return ResponseResult.success(); + } + + /** + * 列出不与指定字段数据存在多对多关系的 [验证规则] 列表数据。通常用于查看添加新 [验证规则] 对象的候选列表。 + * + * @param columnId 主表关联字段。 + * @param onlineRuleDtoFilter [验证规则] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/listNotInOnlineColumnRule") + public ResponseResult> listNotInOnlineColumnRule( + @MyRequestBody Long columnId, + @MyRequestBody OnlineRuleDto onlineRuleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineRule filter = MyModelUtil.copyTo(onlineRuleDtoFilter, OnlineRule.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineRule.class); + List onlineRuleList = + onlineRuleService.getNotInOnlineRuleListByColumnId(columnId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineRuleList, OnlineRuleVo.class)); + } + + /** + * 列出与指定字段数据存在多对多关系的 [验证规则] 列表数据。 + * + * @param columnId 主表关联字段。 + * @param onlineRuleDtoFilter [验证规则] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/listOnlineColumnRule") + public ResponseResult> listOnlineColumnRule( + @MyRequestBody Long columnId, + @MyRequestBody OnlineRuleDto onlineRuleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineRule filter = MyModelUtil.copyTo(onlineRuleDtoFilter, OnlineRule.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineRule.class); + List onlineRuleList = + onlineRuleService.getOnlineRuleListByColumnId(columnId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineRuleList, OnlineRuleVo.class)); + } + + /** + * 批量添加字段数据和 [验证规则] 对象的多对多关联关系数据。 + * + * @param columnId 主表主键Id。 + * @param onlineColumnRuleDtoList 关联对象列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addOnlineColumnRule") + public ResponseResult addOnlineColumnRule( + @MyRequestBody Long columnId, @MyRequestBody List onlineColumnRuleDtoList) { + if (MyCommonUtil.existBlankArgument(columnId, onlineColumnRuleDtoList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + String errorMessage; + for (OnlineColumnRuleDto onlineColumnRule : onlineColumnRuleDtoList) { + errorMessage = MyCommonUtil.getModelValidationError(onlineColumnRule); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + Set ruleIdSet = onlineColumnRuleDtoList.stream() + .map(OnlineColumnRuleDto::getRuleId).collect(Collectors.toSet()); + List ruleList = onlineRuleService.getInList(ruleIdSet); + if (ruleIdSet.size() != ruleList.size()) { + errorMessage = "数据验证失败,参数中存在非法字段规则Id!"; + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID, errorMessage); + } + for (OnlineRule rule : ruleList) { + if (BooleanUtil.isFalse(rule.getBuiltin()) + && !StrUtil.equals(rule.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,参数中存在不属于该应用的字段规则Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + List onlineColumnRuleList = + MyModelUtil.copyCollectionTo(onlineColumnRuleDtoList, OnlineColumnRule.class); + onlineColumnService.addOnlineColumnRuleList(onlineColumnRuleList, columnId); + return ResponseResult.success(); + } + + /** + * 更新指定字段数据和指定 [验证规则] 的多对多关联数据。 + * + * @param onlineColumnRuleDto 对多对中间表对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateOnlineColumnRule") + public ResponseResult updateOnlineColumnRule(@MyRequestBody OnlineColumnRuleDto onlineColumnRuleDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineColumnRuleDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult verifyResult = this.doVerifyColumn(onlineColumnRuleDto.getColumnId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineColumnRule onlineColumnRule = MyModelUtil.copyTo(onlineColumnRuleDto, OnlineColumnRule.class); + if (!onlineColumnService.updateOnlineColumnRule(onlineColumnRule)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 显示字段数据和指定 [验证规则] 的多对多关联详情数据。 + * + * @param columnId 主表主键Id。 + * @param ruleId 从表主键Id。 + * @return 应答结果对象,包括中间表详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/viewOnlineColumnRule") + public ResponseResult viewOnlineColumnRule( + @RequestParam Long columnId, @RequestParam Long ruleId) { + if (MyCommonUtil.existBlankArgument(columnId, ruleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineColumnRule onlineColumnRule = onlineColumnService.getOnlineColumnRule(columnId, ruleId); + if (onlineColumnRule == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + OnlineColumnRuleVo onlineColumnRuleVo = MyModelUtil.copyTo(onlineColumnRule, OnlineColumnRuleVo.class); + return ResponseResult.success(onlineColumnRuleVo); + } + + /** + * 移除指定字段数据和指定 [验证规则] 的多对多关联关系。 + * + * @param columnId 主表主键Id。 + * @param ruleId 从表主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteOnlineColumnRule") + public ResponseResult deleteOnlineColumnRule(@MyRequestBody Long columnId, @MyRequestBody Long ruleId) { + if (MyCommonUtil.existBlankArgument(columnId, ruleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineColumnService.removeOnlineColumnRule(columnId, ruleId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 以字典形式返回全部字段数据数据集合。字典的键值为[columnId, columnName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject OnlineColumnDto filter) { + List resultList = + onlineColumnService.getListByFilter(MyModelUtil.copyTo(filter, OnlineColumn.class)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, OnlineColumn::getColumnId, OnlineColumn::getColumnName)); + } + + private ResponseResult doVerifyColumn(Long columnId) { + if (MyCommonUtil.existBlankArgument(columnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineColumn onlineColumn = onlineColumnService.getById(columnId); + if (onlineColumn == null) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + ResponseResult verifyResult = this.doVerifyTable(onlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyColumn(OnlineColumn onlineColumn, OnlineColumn originalOnlineColumn) { + String errorMessage; + if (onlineColumn.getDictId() != null + && ObjectUtil.notEqual(onlineColumn.getDictId(), originalOnlineColumn.getDictId())) { + OnlineDict dict = onlineDictService.getById(onlineColumn.getDictId()); + if (dict == null) { + errorMessage = "数据验证失败,关联的字典Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!StrUtil.equals(dict.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,关联的字典Id并不属于当前应用!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + } + if (MyCommonUtil.equalsAny(onlineColumn.getFieldKind(), FieldKind.UPLOAD, FieldKind.UPLOAD_IMAGE) + && onlineColumn.getUploadFileSystemType() == null) { + errorMessage = "数据验证失败,上传字段必须设置上传文件系统类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.equal(onlineColumn.getFieldKind(), FieldKind.MASK_FIELD)) { + if (onlineColumn.getMaskFieldType() == null) { + errorMessage = "数据验证失败,脱敏字段没有设置脱敏类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!EnumUtil.contains(MaskFieldTypeEnum.class, onlineColumn.getMaskFieldType())) { + errorMessage = "数据验证失败,脱敏字段设置的脱敏类型并不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + if (!onlineColumn.getTableId().equals(originalOnlineColumn.getTableId())) { + errorMessage = "数据验证失败,字段的所属表Id不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyTable(Long tableId) { + String errorMessage; + OnlineTable table = onlineTableService.getById(tableId); + if (table == null) { + errorMessage = "数据验证失败,指定的数据表Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(table.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用并不包含该字段所在的表!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java new file mode 100644 index 00000000..18831b3b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java @@ -0,0 +1,287 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDatasourceDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.PageType; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineDatasourceVo; +import com.orangeforms.common.online.vo.OnlineTableVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单数据源接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据源接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDatasource") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDatasourceController { + + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineDblinkService onlineDblinkService; + + /** + * 新增数据模型数据。 + * + * @param onlineDatasourceDto 新增对象。 + * @param pageId 关联的页面Id。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineDatasourceDto.datasourceId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody OnlineDatasourceDto onlineDatasourceDto, + @MyRequestBody(required = true) Long pageId) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDatasourceDto, Default.class, AddGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlinePage onlinePage = onlinePageService.getById(pageId); + if (onlinePage == null) { + errorMessage = "数据验证失败,页面Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + if (!StrUtil.equals(onlinePage.getAppCode(), appCode)) { + errorMessage = "数据验证失败,当前应用并不存在该页面!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasource onlineDatasource = MyModelUtil.copyTo(onlineDatasourceDto, OnlineDatasource.class); + if (onlineDatasourceService.existByVariableName(onlineDatasource.getVariableName())) { + errorMessage = "数据验证失败,当前数据源变量已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + OnlineDblink onlineDblink = onlineDblinkService.getById(onlineDatasourceDto.getDblinkId()); + if (onlineDblink == null) { + errorMessage = "数据验证失败,关联的数据库链接Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(onlineDblink.getAppCode(), appCode)) { + errorMessage = "数据验证失败,当前应用并不存在该数据库链接!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SqlTable sqlTable = onlineDblinkService.getDblinkTable(onlineDblink, onlineDatasourceDto.getMasterTableName()); + if (sqlTable == null) { + errorMessage = "数据验证失败,指定的数据表名不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult verifyResult = this.doVerifyPrimaryKey(sqlTable, onlinePage); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + try { + onlineDatasource = onlineDatasourceService.saveNew(onlineDatasource, sqlTable, pageId); + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的数据源变量名已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(onlineDatasource.getDatasourceId()); + } + + /** + * 更新数据模型数据。 + * + * @param onlineDatasourceDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDatasourceDto onlineDatasourceDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDatasourceDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasource onlineDatasource = MyModelUtil.copyTo(onlineDatasourceDto, OnlineDatasource.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineDatasource.getDatasourceId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasource originalOnlineDatasource = verifyResult.getData(); + if (!onlineDatasource.getDblinkId().equals(originalOnlineDatasource.getDblinkId())) { + errorMessage = "数据验证失败,不能修改数据库链接Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasource.getMasterTableId().equals(originalOnlineDatasource.getMasterTableId())) { + errorMessage = "数据验证失败,不能修改主表Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!StrUtil.equals(onlineDatasource.getVariableName(), originalOnlineDatasource.getVariableName()) + && onlineDatasourceService.existByVariableName(onlineDatasource.getVariableName())) { + errorMessage = "数据验证失败,当前数据源变量已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + try { + if (!onlineDatasourceService.update(onlineDatasource, originalOnlineDatasource)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的数据源变量名已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除数据模型数据。 + * + * @param datasourceId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long datasourceId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyAndGet(datasourceId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + List formList = onlineFormService.getOnlineFormListByDatasourceId(datasourceId); + if (CollUtil.isNotEmpty(formList)) { + errorMessage = "数据验证失败,当前数据源正在被 [" + formList.get(0).getFormName() + "] 表单占用,请先删除关联数据!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceService.remove(datasourceId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的数据模型列表。 + * + * @param onlineDatasourceDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDatasourceDto onlineDatasourceDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDatasource onlineDatasourceFilter = MyModelUtil.copyTo(onlineDatasourceDtoFilter, OnlineDatasource.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDatasource.class); + List onlineDatasourceList = + onlineDatasourceService.getOnlineDatasourceListWithRelation(onlineDatasourceFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDatasourceList, OnlineDatasourceVo.class)); + } + + /** + * 查看指定数据模型对象详情。 + * + * @param datasourceId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long datasourceId) { + ResponseResult verifyResult = this.doVerifyAndGet(datasourceId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasource onlineDatasource = + onlineDatasourceService.getByIdWithRelation(datasourceId, MyRelationParam.full()); + OnlineDatasourceVo onlineDatasourceVo = MyModelUtil.copyTo(onlineDatasource, OnlineDatasourceVo.class); + List tableList = onlineTableService.getOnlineTableListByDatasourceId(datasourceId); + if (CollUtil.isNotEmpty(tableList)) { + onlineDatasourceVo.setTableList(MyModelUtil.copyCollectionTo(tableList, OnlineTableVo.class)); + } + return ResponseResult.success(onlineDatasourceVo); + } + + private ResponseResult doVerifyAndGet(Long datasourceId) { + if (MyCommonUtil.existBlankArgument(datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDatasource onlineDatasource = onlineDatasourceService.getById(datasourceId); + if (onlineDatasource == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(onlineDatasource.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用并不存在该数据源!"); + } + return ResponseResult.success(onlineDatasource); + } + + private ResponseResult doVerifyPrimaryKey(SqlTable sqlTable, OnlinePage onlinePage) { + String errorMessage; + boolean hasPrimaryKey = false; + for (SqlTableColumn tableColumn : sqlTable.getColumnList()) { + if (BooleanUtil.isFalse(tableColumn.getPrimaryKey())) { + continue; + } + if (hasPrimaryKey) { + errorMessage = "数据验证失败,数据表只能包含一个主键字段!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + hasPrimaryKey = true; + // 流程表单的主表主键,不能是自增主键。 + if (BooleanUtil.isTrue(tableColumn.getAutoIncrement()) + && onlinePage.getPageType().equals(PageType.FLOW)) { + errorMessage = "数据验证失败,流程页面所关联的主表主键,不能是自增主键!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult verifyResult = onlineColumnService.verifyPrimaryKey(tableColumn); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + } + if (!hasPrimaryKey) { + errorMessage = "数据验证失败,数据表必须包含主键字段!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java new file mode 100644 index 00000000..31755e57 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java @@ -0,0 +1,260 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDatasourceRelationDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineDatasourceRelationVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单数据源关联接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据源关联接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDatasourceRelation") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDatasourceRelationController { + + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineFormService onlineFormService; + + /** + * 新增数据关联数据。 + * + * @param onlineDatasourceRelationDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineDatasourceRelationDto.relationId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDto) { + String errorMessage = MyCommonUtil.getModelValidationError( + onlineDatasourceRelationDto, Default.class, AddGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasourceRelation onlineDatasourceRelation = + MyModelUtil.copyTo(onlineDatasourceRelationDto, OnlineDatasourceRelation.class); + OnlineDatasource onlineDatasource = + onlineDatasourceService.getById(onlineDatasourceRelationDto.getDatasourceId()); + if (onlineDatasource == null) { + errorMessage = "数据验证失败,关联的数据源Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + if (!StrUtil.equals(onlineDatasource.getAppCode(), appCode)) { + errorMessage = "数据验证失败,当前应用并不包含该数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDblink onlineDblink = onlineDblinkService.getById(onlineDatasource.getDblinkId()); + SqlTable slaveTable = onlineDblinkService.getDblinkTable( + onlineDblink, onlineDatasourceRelationDto.getSlaveTableName()); + if (slaveTable == null) { + errorMessage = "数据验证失败,指定的数据表不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SqlTableColumn slaveColumn = null; + for (SqlTableColumn column : slaveTable.getColumnList()) { + if (column.getColumnName().equals(onlineDatasourceRelationDto.getSlaveColumnName())) { + slaveColumn = column; + break; + } + } + if (slaveColumn == null) { + errorMessage = "数据验证失败,指定的数据表字段 [" + onlineDatasourceRelationDto.getSlaveColumnName() + "] 不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = + onlineDatasourceRelationService.verifyRelatedData(onlineDatasourceRelation, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + onlineDatasourceRelation = onlineDatasourceRelationService.saveNew(onlineDatasourceRelation, slaveTable, slaveColumn); + return ResponseResult.success(onlineDatasourceRelation.getRelationId()); + } + + /** + * 更新数据关联数据。 + * + * @param onlineDatasourceRelationDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDto) { + String errorMessage = MyCommonUtil.getModelValidationError( + onlineDatasourceRelationDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasourceRelation onlineDatasourceRelation = + MyModelUtil.copyTo(onlineDatasourceRelationDto, OnlineDatasourceRelation.class); + ResponseResult verifyResult = + this.doVerifyAndGet(onlineDatasourceRelation.getRelationId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation originalOnlineDatasourceRelation = verifyResult.getData(); + if (!onlineDatasourceRelationDto.getRelationType().equals(originalOnlineDatasourceRelation.getRelationType())) { + errorMessage = "数据验证失败,不能修改关联类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceRelationDto.getSlaveTableId().equals(originalOnlineDatasourceRelation.getSlaveTableId())) { + errorMessage = "数据验证失败,不能修改从表Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceRelationDto.getDatasourceId().equals(originalOnlineDatasourceRelation.getDatasourceId())) { + errorMessage = "数据验证失败,不能修改数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = onlineDatasourceRelationService + .verifyRelatedData(onlineDatasourceRelation, originalOnlineDatasourceRelation); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!onlineDatasourceRelationService.update(onlineDatasourceRelation, originalOnlineDatasourceRelation)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除数据关联数据。 + * + * @param relationId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long relationId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation onlineDatasourceRelation = verifyResult.getData(); + OnlineVirtualColumn virtualColumnFilter = new OnlineVirtualColumn(); + virtualColumnFilter.setRelationId(relationId); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnList(virtualColumnFilter, null); + if (CollUtil.isNotEmpty(virtualColumnList)) { + OnlineVirtualColumn virtualColumn = virtualColumnList.get(0); + errorMessage = "数据验证失败,数据源关联正在被虚拟字段 [" + virtualColumn.getColumnPrompt() + "] 使用,不能被删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + List formList = + onlineFormService.getOnlineFormListByTableId(onlineDatasourceRelation.getSlaveTableId()); + if (CollUtil.isNotEmpty(formList)) { + errorMessage = "数据验证失败,当前数据源关联正在被 [" + formList.get(0).getFormName() + "] 表单占用,请先删除关联数据!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceRelationService.remove(relationId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的数据关联列表。 + * + * @param onlineDatasourceRelationDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分 页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDatasourceRelation onlineDatasourceRelationFilter = + MyModelUtil.copyTo(onlineDatasourceRelationDtoFilter, OnlineDatasourceRelation.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDatasourceRelation.class); + List onlineDatasourceRelationList = + onlineDatasourceRelationService.getOnlineDatasourceRelationListWithRelation(onlineDatasourceRelationFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDatasourceRelationList, OnlineDatasourceRelationVo.class)); + } + + /** + * 查看指定数据关联对象详情。 + * + * @param relationId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long relationId) { + ResponseResult verifyResult = this.doVerifyAndGet(relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation onlineDatasourceRelation = + onlineDatasourceRelationService.getByIdWithRelation(relationId, MyRelationParam.full()); + return ResponseResult.success(onlineDatasourceRelation, OnlineDatasourceRelationVo.class); + } + + private ResponseResult doVerifyAndGet(Long relationId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(relationId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDatasourceRelation relation = + onlineDatasourceRelationService.getByIdWithRelation(relationId, MyRelationParam.full()); + if (relation == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(relation.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该数据源关联!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(relation); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java new file mode 100644 index 00000000..60447f1e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java @@ -0,0 +1,276 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDblinkDto; +import com.orangeforms.common.online.model.OnlineDblink; +import com.orangeforms.common.online.service.OnlineDblinkService; +import com.orangeforms.common.online.util.OnlineDataSourceUtil; +import com.orangeforms.common.online.vo.OnlineDblinkVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 在线表单数据库链接接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据库链接接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDblink") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDblinkController { + + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + /** + * 新增数据库链接数据。 + * + * @param onlineDblinkDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @SaCheckPermission("onlineDblink.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineDblinkDto onlineDblinkDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDblinkDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDblink onlineDblink = MyModelUtil.copyTo(onlineDblinkDto, OnlineDblink.class); + onlineDblink = onlineDblinkService.saveNew(onlineDblink); + return ResponseResult.success(onlineDblink.getDblinkId()); + } + + /** + * 更新数据库链接数据。 + * + * @param onlineDblinkDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDblink.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDblinkDto onlineDblinkDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDblinkDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDblink onlineDblink = MyModelUtil.copyTo(onlineDblinkDto, OnlineDblink.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineDblinkDto.getDblinkId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDblink originalOnlineDblink = verifyResult.getData(); + if (ObjectUtil.notEqual(onlineDblink.getDblinkType(), originalOnlineDblink.getDblinkType())) { + errorMessage = "数据验证失败,不能修改数据库类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + String passwdKey = "password"; + JSONObject configJson = JSON.parseObject(onlineDblink.getConfiguration()); + String password = configJson.getString(passwdKey); + if (StrUtil.isNotBlank(password) && StrUtil.isAllCharMatch(password, c -> '*' == c)) { + password = JSON.parseObject(originalOnlineDblink.getConfiguration()).getString(passwdKey); + configJson.put(passwdKey, password); + onlineDblink.setConfiguration(configJson.toJSONString()); + } + if (!onlineDblinkService.update(onlineDblink, originalOnlineDblink)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除数据库链接数据。 + * + * @param dblinkId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDblink.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long dblinkId) { + String errorMessage; + // 验证关联Id的数据合法性 + ResponseResult verifyResult = this.doVerifyAndGet(dblinkId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineDblinkService.remove(dblinkId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的数据库链接列表。 + * + * @param onlineDblinkDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlineDblink.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDblinkDto onlineDblinkDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDblink onlineDblinkFilter = MyModelUtil.copyTo(onlineDblinkDtoFilter, OnlineDblink.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDblink.class); + List onlineDblinkList = + onlineDblinkService.getOnlineDblinkListWithRelation(onlineDblinkFilter, orderBy); + for (OnlineDblink dblink : onlineDblinkList) { + this.maskOffPassword(dblink); + } + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDblinkList, OnlineDblinkVo.class)); + } + + /** + * 查看指定数据库链接对象详情。 + * + * @param dblinkId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlineDblink.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long dblinkId) { + ResponseResult verifyResult = this.doVerifyAndGet(dblinkId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDblink onlineDblink = verifyResult.getData(); + onlineDblinkService.buildRelationForData(onlineDblink, MyRelationParam.full()); + if (!StrUtil.equals(onlineDblink.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用并不存在该数据库链接!"); + } + this.maskOffPassword(onlineDblink); + return ResponseResult.success(onlineDblink, OnlineDblinkVo.class); + } + + /** + * 获取指定数据库链接下的所有动态表单依赖的数据表列表。 + * + * @param dblinkId 数据库链接Id。 + * @return 所有动态表单依赖的数据表列表 + */ + @SaCheckPermission("onlineDblink.all") + @GetMapping("/listDblinkTables") + public ResponseResult> listDblinkTables(@RequestParam Long dblinkId) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineDblinkService.getDblinkTableList(dblink)); + } + + /** + * 获取指定数据库链接下,指定数据表的所有字段信息。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名。 + * @return 该表的所有字段列表。 + */ + @SaCheckPermission("onlineDblink.all") + @GetMapping("/listDblinkTableColumns") + public ResponseResult> listDblinkTableColumns( + @RequestParam Long dblinkId, @RequestParam String tableName) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineDblinkService.getDblinkTableColumnList(dblink, tableName)); + } + + /** + * 测试数据库链接的接口。 + * + * @return 应答结果。 + */ + @GetMapping("/testConnection") + public ResponseResult testConnection(@RequestParam Long dblinkId) { + ResponseResult verifyAndGet = this.doVerifyAndGet(dblinkId); + if (!verifyAndGet.isSuccess()) { + return ResponseResult.errorFrom(verifyAndGet); + } + try { + dataSourceUtil.testConnection(dblinkId); + return ResponseResult.success(); + } catch (Exception e) { + log.error("Failed to test connection with ONLINE_DBLINK_ID [" + dblinkId + "]!", e); + return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "数据库连接失败!"); + } + } + + /** + * 以字典形式返回全部数据库链接数据集合。字典的键值为[dblinkId, dblinkName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject OnlineDblinkDto filter) { + List resultList = + onlineDblinkService.getOnlineDblinkList(MyModelUtil.copyTo(filter, OnlineDblink.class), null); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, OnlineDblink::getDblinkId, OnlineDblink::getDblinkName)); + } + + private ResponseResult doVerifyAndGet(Long dblinkId) { + if (MyCommonUtil.existBlankArgument(dblinkId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDblink onlineDblink = onlineDblinkService.getById(dblinkId); + if (onlineDblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(onlineDblink.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error( + ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用并不存在该数据库链接!"); + } + return ResponseResult.success(onlineDblink); + } + + private void maskOffPassword(OnlineDblink dblink) { + String passwdKey = "password"; + JSONObject configJson = JSON.parseObject(dblink.getConfiguration()); + if (configJson.containsKey(passwdKey)) { + String password = configJson.getString(passwdKey); + if (StrUtil.isNotBlank(password)) { + configJson.put(passwdKey, StrUtil.repeat('*', password.length())); + dblink.setConfiguration(configJson.toJSONString()); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java new file mode 100644 index 00000000..3b31c21b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java @@ -0,0 +1,221 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dict.dto.GlobalDictDto; +import com.orangeforms.common.dict.util.GlobalDictOperationHelper; +import com.orangeforms.common.dict.vo.GlobalDictVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDictDto; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDict; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.service.OnlineDictService; +import com.orangeforms.common.online.vo.OnlineDictVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单字典接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单字典接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDict") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDictController { + + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private GlobalDictOperationHelper globalDictOperationHelper; + + /** + * 新增在线表单字典数据。 + * + * @param onlineDictDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineDictDto.dictId"}) + @SaCheckPermission("onlineDict.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineDictDto onlineDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDictDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDict onlineDict = MyModelUtil.copyTo(onlineDictDto, OnlineDict.class); + // 验证关联Id的数据合法性 + CallResult callResult = onlineDictService.verifyRelatedData(onlineDict, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + onlineDict = onlineDictService.saveNew(onlineDict); + return ResponseResult.success(onlineDict.getDictId()); + } + + /** + * 更新在线表单字典数据。 + * + * @param onlineDictDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDict.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDictDto onlineDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDictDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDict onlineDict = MyModelUtil.copyTo(onlineDictDto, OnlineDict.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineDict.getDictId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDict originalOnlineDict = verifyResult.getData(); + // 验证关联Id的数据合法性 + CallResult callResult = onlineDictService.verifyRelatedData(onlineDict, originalOnlineDict); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!onlineDictService.update(onlineDict, originalOnlineDict)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除在线表单字典数据。 + * + * @param dictId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDict.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long dictId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(dictId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineColumn filter = new OnlineColumn(); + filter.setDictId(dictId); + List columns = onlineColumnService.getListByFilter(filter); + if (CollUtil.isNotEmpty(columns)) { + OnlineColumn usingColumn = columns.get(0); + OnlineTable table = onlineTableService.getById(usingColumn.getTableId()); + errorMessage = String.format("数据验证失败,数据表 [%s] 字段 [%s] 正在引用该字典,因此不能直接删除!", + table.getTableName(), usingColumn.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!onlineDictService.remove(dictId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的在线表单字典列表。 + * + * @param onlineDictDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlineDict.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDictDto onlineDictDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDict onlineDictFilter = MyModelUtil.copyTo(onlineDictDtoFilter, OnlineDict.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDict.class); + List onlineDictList = onlineDictService.getOnlineDictListWithRelation(onlineDictFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDictList, OnlineDictVo.class)); + } + + /** + * 查看指定在线表单字典对象详情。 + * + * @param dictId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlineDict.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long dictId) { + ResponseResult verifyResult = this.doVerifyAndGet(dictId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDict onlineDict = onlineDictService.getByIdWithRelation(dictId, MyRelationParam.full()); + if (onlineDict == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineDict, OnlineDictVo.class); + } + + /** + * 获取全部编码字典列表。 + * NOTE: 白名单接口。 + * + * @param globalDictDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 字典的数据列表。 + */ + @PostMapping("/listAllGlobalDict") + public ResponseResult> listAllGlobalDict( + @MyRequestBody GlobalDictDto globalDictDtoFilter, + @MyRequestBody MyPageParam pageParam) { + return globalDictOperationHelper.listAllGlobalDict(globalDictDtoFilter, pageParam); + } + + private ResponseResult doVerifyAndGet(Long dictId) { + if (MyCommonUtil.existBlankArgument(dictId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDict originalDict = onlineDictService.getById(dictId); + if (originalDict == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(originalDict.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error( + ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用不存在该在线表单字典!"); + } + return ResponseResult.success(originalDict); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java new file mode 100644 index 00000000..921ffee7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java @@ -0,0 +1,428 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dto.OnlineFormDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineFormVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.validation.groups.Default; +import java.util.HashSet; +import java.util.List; +import java.util.LinkedList; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 在线表单表单接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单表单接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineForm") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineFormController { + + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineRuleService onlineRuleService; + @Autowired + private OnlineProperties properties; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + /** + * 新增在线表单数据。 + * + * @param onlineFormDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineFormDto.formId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineFormDto onlineFormDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineFormDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineForm onlineForm = MyModelUtil.copyTo(onlineFormDto, OnlineForm.class); + if (onlineFormService.existByFormCode(onlineForm.getFormCode())) { + errorMessage = "数据验证失败,表单编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = onlineFormService.verifyRelatedData(onlineForm, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Set datasourceIdSet = null; + if (CollUtil.isNotEmpty(onlineFormDto.getDatasourceIdList())) { + ResponseResult> verifyDatasourceIdsResult = + this.doVerifyDatasourceIdsAndGet(onlineFormDto.getDatasourceIdList()); + if (!verifyDatasourceIdsResult.isSuccess()) { + return ResponseResult.errorFrom(verifyDatasourceIdsResult); + } + datasourceIdSet = verifyDatasourceIdsResult.getData(); + } + onlineForm = onlineFormService.saveNew(onlineForm, datasourceIdSet); + return ResponseResult.success(onlineForm.getFormId()); + } + + /** + * 更新在线表单数据。 + * + * @param onlineFormDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineFormDto onlineFormDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineFormDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineForm onlineForm = MyModelUtil.copyTo(onlineFormDto, OnlineForm.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineForm.getFormId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineForm originalOnlineForm = verifyResult.getData(); + // 验证关联Id的数据合法性 + CallResult callResult = onlineFormService.verifyRelatedData(onlineForm, originalOnlineForm); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(onlineForm.getFormCode(), originalOnlineForm.getFormCode()) + && onlineFormService.existByFormCode(onlineForm.getFormCode())) { + errorMessage = "数据验证失败,表单编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + Set datasourceIdSet = null; + if (CollUtil.isNotEmpty(onlineFormDto.getDatasourceIdList())) { + ResponseResult> verifyDatasourceIdsResult = + this.doVerifyDatasourceIdsAndGet(onlineFormDto.getDatasourceIdList()); + if (!verifyDatasourceIdsResult.isSuccess()) { + return ResponseResult.errorFrom(verifyDatasourceIdsResult); + } + datasourceIdSet = verifyDatasourceIdsResult.getData(); + } + if (!onlineFormService.update(onlineForm, originalOnlineForm, datasourceIdSet)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除在线表单数据。 + * + * @param formId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long formId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(formId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineFormService.remove(formId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 克隆一个在线表单对象。 + * + * @param formId 源表单主键Id。 + * @return 新克隆表单主键Id。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/clone") + public ResponseResult clone(@MyRequestBody Long formId) { + ResponseResult verifyResult = this.doVerifyAndGet(formId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineForm form = verifyResult.getData(); + form.setFormName(form.getFormName() + "_copy"); + form.setFormCode(form.getFormCode() + "_copy_" + System.currentTimeMillis()); + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + Set datasourceIdSet = formDatasourceList.stream() + .map(OnlineFormDatasource::getDatasourceId).collect(Collectors.toSet()); + onlineFormService.saveNew(form, datasourceIdSet); + return ResponseResult.success(form.getFormId()); + } + + /** + * 列出符合过滤条件的在线表单列表。 + * + * @param onlineFormDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineFormDto onlineFormDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineForm onlineFormFilter = MyModelUtil.copyTo(onlineFormDtoFilter, OnlineForm.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineForm.class); + List onlineFormList = + onlineFormService.getOnlineFormListWithRelation(onlineFormFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineFormList, OnlineFormVo.class)); + } + + /** + * 查看指定在线表单对象详情。 + * + * @param formId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long formId) { + ResponseResult verifyResult = this.doVerifyAndGet(formId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineForm onlineForm = onlineFormService.getByIdWithRelation(formId, MyRelationParam.full()); + OnlineFormVo onlineFormVo = MyModelUtil.copyTo(onlineForm, OnlineFormVo.class); + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + if (CollUtil.isNotEmpty(formDatasourceList)) { + onlineFormVo.setDatasourceIdList(formDatasourceList.stream() + .map(OnlineFormDatasource::getDatasourceId).collect(Collectors.toList())); + } + return ResponseResult.success(onlineFormVo); + } + + /** + * 获取指定在线表单对象在前端渲染时所需的所有数据对象。 + * + * @param formId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @GetMapping("/render") + public ResponseResult render(@RequestParam Long formId) { + String errorMessage; + Cache cache = null; + if (BooleanUtil.isTrue(properties.getEnableRenderCache())) { + cache = cacheManager.getCache(CacheConfig.CacheEnum.ONLINE_FORM_RENDER_CACCHE.name()); + Assert.notNull(cache, "Cache ONLINE_FORM_RENDER_CACCHE can't be NULL"); + JSONObject responseData = cache.get(formId, JSONObject.class); + if (responseData != null) { + Object appCode = responseData.get("appCode"); + if (ObjectUtil.notEqual(appCode, TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该表单Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(responseData); + } + } + OnlineForm onlineForm = onlineFormService.getOnlineFormFromCache(formId); + if (onlineForm == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + OnlineFormVo onlineFormVo = MyModelUtil.copyTo(onlineForm, OnlineFormVo.class); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("onlineForm", onlineFormVo); + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + if (CollUtil.isEmpty(formDatasourceList)) { + return ResponseResult.success(jsonObject); + } + Set datasourceIdSet = formDatasourceList.stream() + .map(OnlineFormDatasource::getDatasourceId).collect(Collectors.toSet()); + List onlineDatasourceList = + onlineDatasourceService.getOnlineDatasourceListFromCache(datasourceIdSet); + jsonObject.put("onlineDatasourceList", onlineDatasourceList); + Set tableIdSet = onlineDatasourceList.stream() + .map(OnlineDatasource::getMasterTableId).collect(Collectors.toSet()); + List onlineDatasourceRelationList = + onlineDatasourceRelationService.getOnlineDatasourceRelationListFromCache(datasourceIdSet); + if (CollUtil.isNotEmpty(onlineDatasourceRelationList)) { + jsonObject.put("onlineDatasourceRelationList", onlineDatasourceRelationList); + tableIdSet.addAll(onlineDatasourceRelationList.stream() + .map(OnlineDatasourceRelation::getSlaveTableId).collect(Collectors.toList())); + } + List onlineTableList = new LinkedList<>(); + List onlineColumnList = new LinkedList<>(); + for (Long tableId : tableIdSet) { + OnlineTable table = onlineTableService.getOnlineTableFromCache(tableId); + onlineTableList.add(table); + onlineColumnList.addAll(table.getColumnMap().values()); + table.setColumnMap(null); + } + jsonObject.put("onlineTableList", onlineTableList); + jsonObject.put("onlineColumnList", onlineColumnList); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnListByTableIds(tableIdSet); + jsonObject.put("onlineVirtualColumnList", virtualColumnList); + Set dictIdSet = onlineColumnList.stream() + .filter(c -> c.getDictId() != null).map(OnlineColumn::getDictId).collect(Collectors.toSet()); + Set widgetDictIdSet = this.extractDictIdSetFromWidgetJson(onlineForm.getWidgetJson()); + CollUtil.addAll(dictIdSet, widgetDictIdSet); + if (CollUtil.isNotEmpty(dictIdSet)) { + List onlineDictList = onlineDictService.getOnlineDictListFromCache(dictIdSet); + if (onlineDictList.size() != dictIdSet.size()) { + Set columnDictIdSet = onlineDictList.stream().map(OnlineDict::getDictId).collect(Collectors.toSet()); + Long notExistDictId = this.findNotExistDictId(dictIdSet, columnDictIdSet); + Assert.notNull(notExistDictId, "notExistDictId can't be NULL"); + errorMessage = String.format("数据验证失败,字典Id [%s] 不存在!", notExistDictId); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + jsonObject.put("onlineDictList", onlineDictList); + } + Set columnIdSet = onlineColumnList.stream().map(OnlineColumn::getColumnId).collect(Collectors.toSet()); + List colunmRuleList = onlineRuleService.getOnlineColumnRuleListByColumnIds(columnIdSet); + if (CollUtil.isNotEmpty(colunmRuleList)) { + jsonObject.put("onlineColumnRuleList", colunmRuleList); + } + jsonObject.put("appCode", TokenData.takeFromRequest().getAppCode()); + if (BooleanUtil.isTrue(properties.getEnableRenderCache())) { + Assert.notNull(cache, "Cache ONLINE_FORM_RENDER_CACCHE can't be NULL"); + cache.put(formId, jsonObject); + } + return ResponseResult.success(jsonObject); + } + + private Long findNotExistDictId(Set originalDictIdSet, Set dictIdSet) { + return originalDictIdSet.stream().filter(d -> !dictIdSet.contains(d)).findFirst().orElse(null); + } + + private ResponseResult doVerifyAndGet(Long formId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(formId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineForm form = onlineFormService.getById(formId); + if (form == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(form.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该表单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(form.getTenantId(), TokenData.takeFromRequest().getTenantId())) { + errorMessage = "数据验证失败,当前租户不包含该表单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(form); + } + + private ResponseResult> doVerifyDatasourceIdsAndGet(List datasourceIdList) { + String errorMessage; + Set datasourceIdSet = new HashSet<>(datasourceIdList); + List datasourceList = onlineDatasourceService.getInList(datasourceIdSet); + if (datasourceIdSet.size() != datasourceList.size()) { + errorMessage = "数据验证失败,当前在线表单包含不存在的数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + for (OnlineDatasource datasource : datasourceList) { + if (!StrUtil.equals(datasource.getAppCode(), appCode)) { + errorMessage = "数据验证失败,存在不是当前应用的数据源!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(datasourceIdSet); + } + + private Set extractDictIdSetFromWidgetJson(String widgetJson) { + Set dictIdSet = new HashSet<>(); + if (StrUtil.isBlank(widgetJson)) { + return dictIdSet; + } + JSONObject allData = JSON.parseObject(widgetJson); + JSONObject pcData = allData.getJSONObject("pc"); + if (MapUtil.isEmpty(pcData)) { + return dictIdSet; + } + JSONArray widgetListArray = pcData.getJSONArray("widgetList"); + if (CollUtil.isEmpty(widgetListArray)) { + return dictIdSet; + } + for (int i = 0; i < widgetListArray.size(); i++) { + this.recursiveExtractDictId(widgetListArray.getJSONObject(i), dictIdSet); + } + return dictIdSet; + } + + private void recursiveExtractDictId(JSONObject widgetData, Set dictIdSet) { + JSONObject propsData = widgetData.getJSONObject("props"); + if (MapUtil.isNotEmpty(propsData)) { + JSONObject dictInfoData = propsData.getJSONObject("dictInfo"); + if (MapUtil.isNotEmpty(dictInfoData)) { + Long dictId = dictInfoData.getLong("dictId"); + if (dictId != null) { + dictIdSet.add(dictId); + } + } + } + JSONArray childWidgetArray = widgetData.getJSONArray("childWidgetList"); + if (CollUtil.isNotEmpty(childWidgetArray)) { + for (int i = 0; i < childWidgetArray.size(); i++) { + this.recursiveExtractDictId(childWidgetArray.getJSONObject(i), dictIdSet); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java new file mode 100644 index 00000000..64786ae1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java @@ -0,0 +1,1044 @@ +package com.orangeforms.common.online.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.*; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.DictType; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.exception.OnlineRuntimeException; +import com.orangeforms.common.online.util.OnlineOperationHelper; +import com.orangeforms.common.online.util.OnlineConstant; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线表单数据操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据操作接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineOperation") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineOperationController { + + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineOperationHelper onlineOperationHelper; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private GlobalDictService globalDictService; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private SessionCacheHelper sessionCacheHelper; + + /** + * 新增数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表的数据源Id。 + * @param masterData 主表新增数据。 + * @param slaveData 一对多从表新增数据列表。 + * @return 应答结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/addDatasource/{datasourceVariableName}") + public ResponseResult addDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData) { + // 验证数据源的合法性,同时获取主表对象。 + ResponseResult datasourceResult = onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + OnlineTable masterTable = datasource.getMasterTable(); + if (slaveData == null) { + onlineOperationService.saveNew(masterTable, masterData); + } else { + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasourceId, slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + onlineOperationService.saveNewWithRelation(masterTable, masterData, slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + /** + * 新增一对多从表数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表的数据源Id。 + * @param relationId 一对多的关联Id。 + * @param slaveData 一对多从表的新增数据列表。 + * @return 应答结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/addOneToManyRelation/{datasourceVariableName}") + public ResponseResult addOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) JSONObject slaveData) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation relation = verifyResult.getData(); + onlineOperationService.saveNew(relation.getSlaveTable(), slaveData); + return ResponseResult.success(); + } + + /** + * 更新主数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param masterData 表数据。这里没有包含的字段将视为NULL。 + * @param slaveData 从表数据,key是relationId。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateDatasource/{datasourceVariableName}") + public ResponseResult updateDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + OnlineTable masterTable = datasource.getMasterTable(); + if (slaveData == null) { + if (!onlineOperationService.update(masterTable, masterData)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } else { + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasourceId, slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + onlineOperationService.updateWithRelation( + masterTable, masterData, datasourceId, slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + /** + * 更新一对多关联数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param relationId 一对多关联Id。 + * @param slaveData 一对多关联从表数据。这里没有包含的字段将视为NULL。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateOneToManyRelation/{datasourceVariableName}") + public ResponseResult updateOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) JSONObject slaveData) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineTable slaveTable = verifyResult.getData().getSlaveTable(); + if (!onlineOperationService.update(slaveTable, slaveData)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除主数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param dataId 待删除的数据表主键Id。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/deleteDatasource/{datasourceVariableName}") + public ResponseResult deleteDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) String dataId) { + return this.doDelete(datasourceVariableName, datasourceId, CollUtil.newArrayList(dataId)); + } + + /** + * 批量删除主数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param dataIdList 待删除的数据表主键Id列表。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatchDatasource/{datasourceVariableName}") + public ResponseResult deleteBatchDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) List dataIdList) { + return this.doDelete(datasourceVariableName, datasourceId, dataIdList); + } + + /** + * 删除一对多关联表单条数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param relationId 一对多关联Id。 + * @param dataId 一对多关联表主键Id。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/deleteOneToManyRelation/{datasourceVariableName}") + public ResponseResult deleteOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) String dataId) { + return this.doDelete(datasourceVariableName, datasourceId, relationId, CollUtil.newArrayList(dataId)); + } + + /** + * 批量删除一对多关联表单条数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param relationId 一对多关联Id。 + * @param dataIdList 一对多关联表主键Id列表。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatchOneToManyRelation/{datasourceVariableName}") + public ResponseResult deleteBatchOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) List dataIdList) { + return this.doDelete(datasourceVariableName, datasourceId, relationId, dataIdList); + } + + /** + * 根据数据源Id为动态表单查询数据详情。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param dataId 数据主键Id。 + * @return 详情结果。 + */ + @SaTokenDenyAuth + @GetMapping("/viewByDatasourceId/{datasourceVariableName}") + public ResponseResult> viewByDatasourceId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam String dataId) { + // 验证数据源及其关联 + ResponseResult datasourceResult = + this.doVerifyAndGetDatasource(datasourceId, datasourceVariableName); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + List allRelationList = relationListResult.getData(); + List oneToOneRelationList = allRelationList.stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + Map result = onlineOperationService.getMasterData( + datasource.getMasterTable(), oneToOneRelationList, allRelationList, dataId); + return ResponseResult.success(result); + } + + /** + * 根据数据源关联Id为动态表单查询数据详情。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 一对多关联Id。 + * @param dataId 一对多关联数据主键Id。 + * @return 详情结果。 + */ + @SaTokenDenyAuth + @GetMapping("/viewByOneToManyRelationId/{datasourceVariableName}") + public ResponseResult> viewByOneToManyRelationId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam Long relationId, + @RequestParam String dataId) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + Map result = onlineOperationService.getSlaveData(verifyResult.getData(), dataId); + return ResponseResult.success(result); + } + + /** + * 为数据源主表字段下载文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param dataId 附件所在记录的主键Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param response Http 应答对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/downloadDatasource/{datasourceVariableName}") + public void downloadDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam(required = false) String dataId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) throws IOException { + if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(datasourceResult)); + return; + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return; + } + OnlineTable masterTable = datasource.getMasterTable(); + onlineOperationHelper.doDownload(masterTable, dataId, fieldName, filename, asImage, response); + } + + /** + * 为数据源一对多关联的从表字段下载文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param dataId 附件所在记录的主键Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param response Http 应答对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/downloadOneToManyRelation/{datasourceVariableName}") + public void downloadOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam Long relationId, + @RequestParam(required = false) String dataId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) throws IOException { + ResponseResult relationResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!relationResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(relationResult)); + return; + } + OnlineTable slaveTable = relationResult.getData().getSlaveTable(); + onlineOperationHelper.doDownload(slaveTable, dataId, fieldName, filename, asImage, response); + } + + /** + * 为数据源主表字段上传文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param uploadFile 上传文件对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/uploadDatasource/{datasourceVariableName}") + public void uploadDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(datasourceResult)); + return; + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return; + } + OnlineTable masterTable = datasource.getMasterTable(); + onlineOperationHelper.doUpload(masterTable, fieldName, asImage, uploadFile); + } + + /** + * 为数据源一对多关联的从表字段上传文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param uploadFile 上传文件对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/uploadOneToManyRelation/{datasourceVariableName}") + public void uploadOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam Long relationId, + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + ResponseResult relationResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!relationResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(relationResult)); + return; + } + OnlineTable slaveTable = relationResult.getData().getSlaveTable(); + onlineOperationHelper.doUpload(slaveTable, fieldName, asImage, uploadFile); + } + + /** + * 根据数据源Id,以及接口参数,为动态表单查询数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param pageParam 分页对象。 + */ + @SaTokenDenyAuth + @PostMapping("/listByDatasourceId/{datasourceVariableName}") + public ResponseResult>> listByDatasourceId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + // 1. 验证数据源及其关联 + ResponseResult datasourceResult = + this.doVerifyAndGetDatasource(datasourceId, datasourceVariableName); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineTable masterTable = datasourceResult.getData().getMasterTable(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + List allRelationList = relationListResult.getData(); + // 2. 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + return ResponseResult.errorFrom(filterDtoListResult); + } + // 3. 解析排序参数,同时确保没有sql注入。 + Map tableMap = new HashMap<>(4); + tableMap.put(masterTable.getTableName(), masterTable); + List oneToOneRelationList = relationListResult.getData().stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(oneToOneRelationList)) { + Map relationTableMap = oneToOneRelationList.stream() + .map(OnlineDatasourceRelation::getSlaveTable).collect(Collectors.toMap(OnlineTable::getTableName, c -> c)); + tableMap.putAll(relationTableMap); + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, masterTable, tableMap); + if (!orderByResult.isSuccess()) { + return ResponseResult.errorFrom(orderByResult); + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = onlineOperationService.getMasterDataList( + masterTable, oneToOneRelationList, allRelationList, filterDtoList, orderBy, pageParam); + return ResponseResult.success(pageData); + } + + /** + * 根据数据源Id,以及接口参数,为动态表单导出数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param exportInfoList 导出字段信息列表。 + */ + @SaTokenDenyAuth + @PostMapping("/exportByDatasourceId/{datasourceVariableName}") + public void exportByDatasourceId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody(required = true) List exportInfoList) throws IOException { + // 1. 验证数据源及其关联 + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + } + OnlineTable masterTable = datasource.getMasterTable(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, null); + if (!relationListResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, relationListResult); + } + List allRelationList = relationListResult.getData(); + // 2. 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, filterDtoListResult); + } + // 3. 解析排序参数,同时确保没有sql注入。 + Map tableMap = new HashMap<>(4); + tableMap.put(masterTable.getTableName(), masterTable); + List oneToOneRelationList = relationListResult.getData().stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(oneToOneRelationList)) { + Map relationTableMap = oneToOneRelationList.stream() + .map(OnlineDatasourceRelation::getSlaveTable).collect(Collectors.toMap(OnlineTable::getTableName, c -> c)); + tableMap.putAll(relationTableMap); + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, masterTable, tableMap); + if (!orderByResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, orderByResult); + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = onlineOperationService.getMasterDataList( + masterTable, oneToOneRelationList, allRelationList, filterDtoList, orderBy, null); + Map headerMap = this.makeExportHeaderMap(masterTable, allRelationList, exportInfoList); + if (MapUtil.isEmpty(headerMap)) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, + ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,没有指定导出头信息!")); + return; + } + this.normalizeExportDataList(pageData.getDataList()); + String filename = datasourceVariableName + "-" + MyDateUtil.toDateTimeString(DateTime.now()) + ".xlsx"; + ExportUtil.doExport(pageData.getDataList(), headerMap, filename); + } + + /** + * 根据数据源Id和数据源关联Id,以及接口参数,为动态表单查询该一对多关联的数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param pageParam 分页对象。 + * @return 查询结果。 + */ + @SaTokenDenyAuth + @PostMapping("/listByOneToManyRelationId/{datasourceVariableName}") + public ResponseResult>> listByOneToManyRelationId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation relation = verifyResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + // 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + return ResponseResult.errorFrom(filterDtoListResult); + } + Map tableMap = new HashMap<>(1); + tableMap.put(slaveTable.getTableName(), slaveTable); + if (CollUtil.isNotEmpty(orderParam)) { + for (MyOrderParam.OrderInfo orderInfo : orderParam) { + orderInfo.setFieldName(StrUtil.removePrefix(orderInfo.getFieldName(), + relation.getVariableName() + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR)); + } + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, slaveTable, tableMap); + if (!orderByResult.isSuccess()) { + return ResponseResult.errorFrom(orderByResult); + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = + onlineOperationService.getSlaveDataList(relation, filterDtoList, orderBy, pageParam); + return ResponseResult.success(pageData); + } + + /** + * 根据数据源Id和数据源关联Id,以及接口参数,为动态表单查询该一对多关联的数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param exportInfoList 导出字段信息列表。 + */ + @SaTokenDenyAuth + @PostMapping("/exportByOneToManyRelationId/{datasourceVariableName}") + public void exportByOneToManyRelationId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody(required = true) List exportInfoList) throws IOException { + ResponseResult relationResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!relationResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, relationResult); + return; + } + OnlineDatasourceRelation relation = relationResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + // 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, filterDtoListResult); + return; + } + Map tableMap = new HashMap<>(1); + tableMap.put(slaveTable.getTableName(), slaveTable); + if (CollUtil.isNotEmpty(orderParam)) { + for (MyOrderParam.OrderInfo orderInfo : orderParam) { + orderInfo.setFieldName(StrUtil.removePrefix(orderInfo.getFieldName(), + relation.getVariableName() + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR)); + } + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, slaveTable, tableMap); + if (!orderByResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, orderByResult); + return; + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = + onlineOperationService.getSlaveDataList(relation, filterDtoList, orderBy, null); + Map headerMap = + this.makeExportHeaderMap(relation.getSlaveTable(), null, exportInfoList); + if (MapUtil.isEmpty(headerMap)) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, + ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,没有指定导出头信息!")); + return; + } + this.normalizeExportDataList(pageData.getDataList()); + String filename = datasourceVariableName + "-relation-" + MyDateUtil.toDateTimeString(DateTime.now()) + ".xlsx"; + ExportUtil.doExport(pageData.getDataList(), headerMap, filename); + } + + /** + * 查询字典数据,并以字典的约定方式,返回数据结果集。 + * + * @param dictId 字典Id。 + * @param filterDtoList 字典的过滤对象列表。 + * @return 字典数据列表。 + */ + @PostMapping("/listDict") + public ResponseResult>> listDict( + @MyRequestBody(required = true) Long dictId, + @MyRequestBody List filterDtoList) { + String errorMessage; + OnlineDict dict = onlineDictService.getOnlineDictFromCache(dictId); + if (dict == null) { + errorMessage = "数据验证失败,字典Id并不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(dict.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用并不包含该字典Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!dict.getDictType().equals(DictType.TABLE) + && !dict.getDictType().equals(DictType.GLOBAL_DICT)) { + errorMessage = "数据验证失败,该接口仅支持数据表字典和全局编码字典!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (dict.getDictType().equals(DictType.GLOBAL_DICT)) { + List dictItems = + globalDictService.getGlobalDictItemListFromCache(dict.getDictCode(), null); + List> dataMapList = + MyCommonUtil.toDictDataList(dictItems, GlobalDictItem::getItemId, GlobalDictItem::getItemName); + return ResponseResult.success(dataMapList); + } + if (CollUtil.isNotEmpty(filterDtoList)) { + for (OnlineFilterDto filter : filterDtoList) { + if (!this.checkTableAndColumnName(filter.getColumnName())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤字段名 [{}] 包含 (数字、字母和下划线) 之外的非法字符!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + } + List> resultList = onlineOperationService.getDictDataList(dict, filterDtoList); + return ResponseResult.success(resultList); + } + + /** + * 获取在线表单所关联的权限数据,包括权限字列表和权限资源列表。 + * 注:该接口仅用于微服务间调用使用,无需对前端开放。 + * + * @param menuFormIds 菜单关联的表单Id集合。 + * @param viewFormIds 查询权限的表单Id集合。 + * @param editFormIds 编辑权限的表单Id集合。 + * @return 参数中在线表单所关联的权限数据。 + */ + @GetMapping("/calculatePermData") + public ResponseResult> calculatePermData( + @RequestParam Set menuFormIds, + @RequestParam Set viewFormIds, + @RequestParam Set editFormIds) { + return ResponseResult.success(onlineOperationService.calculatePermData(menuFormIds, viewFormIds, editFormIds)); + } + + private ResponseResult doDelete( + String datasourceVariableName, Long datasourceId, List dataIdList) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + OnlineTable masterTable = datasource.getMasterTable(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, RelationType.ONE_TO_MANY); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + List relationList = relationListResult.getData(); + for (String dataId : dataIdList) { + if (!onlineOperationService.delete(masterTable, relationList, dataId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } + return ResponseResult.success(); + } + + private ResponseResult doDelete( + String datasourceVariableName, Long datasourceId, Long relationId, List dataIdList) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation relation = verifyResult.getData(); + for (String dataId : dataIdList) { + if (!onlineOperationService.delete(relation.getSlaveTable(), null, dataId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyAndGetDatasource( + Long datasourceId, String datasourceVariableName) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + return ResponseResult.success(datasource); + } + + private ResponseResult doVerifyAndGetRelation( + Long datasourceId, String datasourceVariableName, Long relationId) { + OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceFromCache(datasourceId); + if (datasource == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,数据源Id并不存在!"); + } + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + return onlineOperationHelper.verifyAndGetRelation(datasourceId, relationId); + } + + private ResponseResult verifyFilterDtoList(List filterDtoList) { + if (CollUtil.isEmpty(filterDtoList)) { + return ResponseResult.success(); + } + String errorMessage; + for (OnlineFilterDto filter : filterDtoList) { + if (!this.checkTableAndColumnName(filter.getTableName())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤表名 [{}] 包含 (数字、字母和下划线) 之外的非法字符!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!this.checkTableAndColumnName(filter.getColumnName())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤字段名 [{}] 包含 (数字、字母和下划线) 之外的非法字符!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!filter.getFilterType().equals(FieldFilterType.RANGE_FILTER) + && ObjectUtil.isEmpty(filter.getColumnValue())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤字段名 [{}] 过滤值不能为空!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(); + } + + private boolean checkTableAndColumnName(String name) { + if (StrUtil.isBlank(name)) { + return true; + } + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (!CharUtil.isLetterOrNumber(c) && !CharUtil.equals('_', c, false)) { + return false; + } + } + return true; + } + + private ResponseResult makeOrderBy( + MyOrderParam orderParam, OnlineTable masterTable, Map tableMap) { + if (CollUtil.isEmpty(orderParam)) { + return ResponseResult.success(null); + } + String errorMessage; + StringBuilder sb = new StringBuilder(128); + for (MyOrderParam.OrderInfo orderInfo : orderParam) { + String[] orderArray = StrUtil.splitToArray(orderInfo.getFieldName(), '.'); + // 如果没有前缀,我们就可以默认为主表的字段。 + if (orderArray.length == 1) { + try { + sb.append(this.makeOrderByForOrderInfo(masterTable, orderArray[0], orderInfo)); + } catch (OnlineRuntimeException e) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, e.getMessage()); + } + } else { + String tableName = orderArray[0]; + String columnName = orderArray[1]; + OnlineTable table = tableMap.get(tableName); + if (table == null) { + errorMessage = StrFormatter.format( + "数据验证失败,排序字段 [{}] 的数据表 [{}] 并不属于当前数据源!", + orderInfo.getFieldName(), tableName); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + try { + sb.append(this.makeOrderByForOrderInfo(table, columnName, orderInfo)); + } catch (OnlineRuntimeException e) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, e.getMessage()); + } + } + } + return ResponseResult.success(sb.substring(0, sb.length() - 2)); + } + + private String makeOrderByForOrderInfo( + OnlineTable table, String columnName, MyOrderParam.OrderInfo orderInfo) { + StringBuilder sb = new StringBuilder(64); + boolean found = false; + for (OnlineColumn column : table.getColumnMap().values()) { + if (column.getColumnName().equals(columnName)) { + sb.append(table.getTableName()).append(".").append(columnName); + if (BooleanUtil.isFalse(orderInfo.getAsc())) { + sb.append(" DESC"); + } + sb.append(", "); + found = true; + break; + } + } + if (!found) { + String errorMessage = StrFormatter.format( + "数据验证失败,排序字段 [{}] 在数据表 [{}] 中并不存在!", + orderInfo.getFieldName(), table.getTableName()); + throw new OnlineRuntimeException(errorMessage); + } + return sb.toString(); + } + + private int makeImportHeaderInfoByFieldType(String objectFieldType) { + return switch (objectFieldType) { + case ObjectFieldType.INTEGER -> ImportUtil.INT_TYPE; + case ObjectFieldType.LONG -> ImportUtil.LONG_TYPE; + case ObjectFieldType.STRING -> ImportUtil.STRING_TYPE; + case ObjectFieldType.BOOLEAN -> ImportUtil.BOOLEAN_TYPE; + case ObjectFieldType.DATE -> ImportUtil.DATE_TYPE; + case ObjectFieldType.DOUBLE -> ImportUtil.DOUBLE_TYPE; + case ObjectFieldType.BIG_DECIMAL -> ImportUtil.BIG_DECIMAL_TYPE; + default -> throw new MyRuntimeException("Unsupport Import FieldType"); + }; + } + + private Map makeExportHeaderMap( + OnlineTable masterTable, + List allRelationList, + List exportInfoList) { + Map headerMap = new LinkedHashMap<>(16); + Map allRelationMap = null; + if (allRelationList != null) { + allRelationMap = allRelationList.stream() + .collect(Collectors.toMap(OnlineDatasourceRelation::getSlaveTableId, r -> r)); + } + for (ExportInfo exportInfo : exportInfoList) { + if (exportInfo.getVirtualColumnId() != null) { + OnlineVirtualColumn virtualColumn = + onlineVirtualColumnService.getById(exportInfo.getVirtualColumnId()); + if (virtualColumn != null) { + headerMap.put(virtualColumn.getObjectFieldName(), exportInfo.showName); + } + continue; + } + if (masterTable != null && exportInfo.getTableId().equals(masterTable.getTableId())) { + OnlineColumn column = masterTable.getColumnMap().get(exportInfo.getColumnId()); + String columnName = this.appendSuffixForDictColumn(column, column.getColumnName()); + headerMap.put(columnName, exportInfo.getShowName()); + } else { + OnlineDatasourceRelation relation = + MapUtil.get(allRelationMap, exportInfo.getTableId(), OnlineDatasourceRelation.class); + if (relation != null) { + OnlineColumn column = relation.getSlaveTable().getColumnMap().get(exportInfo.getColumnId()); + String columnName = this.appendSuffixForDictColumn( + column, relation.getVariableName() + "." + column.getColumnName()); + headerMap.put(columnName, exportInfo.getShowName()); + } + } + } + return headerMap; + } + + private void normalizeExportDataList(List> dataList) { + for (Map columnData : dataList) { + for (Map.Entry entry : columnData.entrySet()) { + if (entry.getValue() instanceof Long || entry.getValue() instanceof BigDecimal) { + columnData.put(entry.getKey(), entry.getValue() == null ? "" : entry.getValue().toString()); + } + } + } + } + + private String appendSuffixForDictColumn(OnlineColumn column, String columnName) { + if (column.getDictId() != null) { + if (ObjectUtil.equal(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + columnName += "DictMapList"; + } else { + columnName += "DictMap.name"; + } + } + return columnName; + } + + @Data + public static class ExportInfo { + private Long tableId; + private Long columnId; + private Long virtualColumnId; + private String showName; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java new file mode 100644 index 00000000..25bbedb9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java @@ -0,0 +1,386 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDatasourceDto; +import com.orangeforms.common.online.dto.OnlinePageDatasourceDto; +import com.orangeforms.common.online.dto.OnlinePageDto; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineForm; +import com.orangeforms.common.online.model.OnlinePage; +import com.orangeforms.common.online.model.OnlinePageDatasource; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineFormService; +import com.orangeforms.common.online.service.OnlinePageService; +import com.orangeforms.common.online.vo.OnlineDatasourceVo; +import com.orangeforms.common.online.vo.OnlinePageDatasourceVo; +import com.orangeforms.common.online.vo.OnlinePageVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 在线表单页面接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单页面接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlinePage") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlinePageController { + + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + + /** + * 新增在线表单页面数据。 + * + * @param onlinePageDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlinePageDto.pageId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlinePageDto onlinePageDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlinePage onlinePage = MyModelUtil.copyTo(onlinePageDto, OnlinePage.class); + if (onlinePageService.existByPageCode(onlinePage.getPageCode())) { + errorMessage = "数据验证失败,页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + try { + onlinePage = onlinePageService.saveNew(onlinePage); + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(onlinePage.getPageId()); + } + + /** + * 更新在线表单页面数据。 + * + * @param onlinePageDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlinePageDto onlinePageDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlinePage onlinePage = MyModelUtil.copyTo(onlinePageDto, OnlinePage.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlinePage.getPageId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePage originalOnlinePage = verifyResult.getData(); + if (!onlinePage.getPageType().equals(originalOnlinePage.getPageType())) { + errorMessage = "数据验证失败,页面类型不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!StrUtil.equals(onlinePage.getPageCode(), originalOnlinePage.getPageCode()) + && onlinePageService.existByPageCode(onlinePage.getPageCode())) { + errorMessage = "数据验证失败,页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + try { + if (!onlinePageService.update(onlinePage, originalOnlinePage)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 更新在线表单页面对象的发布状态字段。 + * + * @param pageId 待更新的页面对象主键Id。 + * @param published 发布状态。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updatePublished") + public ResponseResult updateStatus( + @MyRequestBody(required = true) Long pageId, + @MyRequestBody(required = true) Boolean published) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePage originalOnlinePage = verifyResult.getData(); + if (!published.equals(originalOnlinePage.getPublished())) { + if (BooleanUtil.isTrue(published) && !originalOnlinePage.getStatus().equals(PageStatus.FORM_DESIGN)) { + errorMessage = "数据验证失败,当前页面状态不为 [设计] 状态,因此不能发布!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + onlinePageService.updatePublished(pageId, published); + } + return ResponseResult.success(); + } + + /** + * 删除在线表单页面数据。 + * + * @param pageId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long pageId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlinePageService.remove(pageId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的在线表单页面列表。 + * + * @param onlinePageDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlinePageDto onlinePageDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlinePage onlinePageFilter = MyModelUtil.copyTo(onlinePageDtoFilter, OnlinePage.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlinePage.class); + List onlinePageList = onlinePageService.getOnlinePageListWithRelation(onlinePageFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlinePageList, OnlinePageVo.class)); + } + + /** + * 获取系统中配置的所有Page和表单的列表。 + * + * @return 系统中配置的所有Page和表单的列表。 + */ + @PostMapping("/listAllPageAndForm") + public ResponseResult listAllPageAndForm() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("pageList", onlinePageService.getOnlinePageList(null, null)); + List formList = onlineFormService.getOnlineFormList(null, null); + formList.forEach(f -> f.setWidgetJson(null)); + jsonObject.put("formList", formList); + return ResponseResult.success(jsonObject); + } + + /** + * 查看指定在线表单页面对象详情。 + * + * @param pageId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long pageId) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePage onlinePage = onlinePageService.getByIdWithRelation(pageId, MyRelationParam.full()); + if (onlinePage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlinePage, OnlinePageVo.class); + } + + /** + * 列出与指定在线表单页面存在多对多关系的在线数据源列表数据。 + * + * @param pageId 主表关联字段。 + * @param onlineDatasourceDtoFilter 在线数据源过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/listOnlinePageDatasource") + public ResponseResult> listOnlinePageDatasource( + @MyRequestBody Long pageId, + @MyRequestBody OnlineDatasourceDto onlineDatasourceDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDatasource filter = MyModelUtil.copyTo(onlineDatasourceDtoFilter, OnlineDatasource.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDatasource.class); + List onlineDatasourceList = + onlineDatasourceService.getOnlineDatasourceListByPageId(pageId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDatasourceList, OnlineDatasourceVo.class)); + } + + /** + * 批量添加在线表单页面和在线数据源对象的多对多关联关系数据。 + * + * @param pageId 主表主键Id。 + * @param onlinePageDatasourceDtoList 关联对象列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addOnlinePageDatasource") + public ResponseResult addOnlinePageDatasource( + @MyRequestBody Long pageId, + @MyRequestBody(value = "onlinePageDatasourceList") List onlinePageDatasourceDtoList) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (MyCommonUtil.existBlankArgument(onlinePageDatasourceDtoList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + for (OnlinePageDatasourceDto onlinePageDatasource : onlinePageDatasourceDtoList) { + errorMessage = MyCommonUtil.getModelValidationError(onlinePageDatasource); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + Set datasourceIdSet = onlinePageDatasourceDtoList.stream() + .map(OnlinePageDatasourceDto::getDatasourceId).collect(Collectors.toSet()); + List datasourceList = onlineDatasourceService.getInList(datasourceIdSet); + if (datasourceIdSet.size() != datasourceList.size()) { + errorMessage = "数据验证失败,当前在线表单包含不存在的数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + for (OnlineDatasource datasource : datasourceList) { + if (!StrUtil.equals(datasource.getAppCode(), appCode)) { + errorMessage = "数据验证失败,存在不是当前应用的数据源!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + List onlinePageDatasourceList = + MyModelUtil.copyCollectionTo(onlinePageDatasourceDtoList, OnlinePageDatasource.class); + onlinePageService.addOnlinePageDatasourceList(onlinePageDatasourceList, pageId); + return ResponseResult.success(); + } + + /** + * 显示在线表单页面和指定数据源的多对多关联详情数据。 + * + * @param pageId 主表主键Id。 + * @param datasourceId 从表主键Id。 + * @return 应答结果对象,包括中间表详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/viewOnlinePageDatasource") + public ResponseResult viewOnlinePageDatasource( + @RequestParam Long pageId, @RequestParam Long datasourceId) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePageDatasource onlinePageDatasource = onlinePageService.getOnlinePageDatasource(pageId, datasourceId); + if (onlinePageDatasource == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + OnlinePageDatasourceVo onlinePageDatasourceVo = + MyModelUtil.copyTo(onlinePageDatasource, OnlinePageDatasourceVo.class); + return ResponseResult.success(onlinePageDatasourceVo); + } + + /** + * 移除指定在线表单页面和指定数据源的多对多关联关系。 + * + * @param pageId 主表主键Id。 + * @param datasourceId 从表主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteOnlinePageDatasource") + public ResponseResult deleteOnlinePageDatasource( + @MyRequestBody Long pageId, @MyRequestBody(required = true) Long datasourceId) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlinePageService.removeOnlinePageDatasource(pageId, datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyAndGet(Long pageId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(pageId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlinePage onlinePage = onlinePageService.getById(pageId); + if (onlinePage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(onlinePage.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用不存在该页面!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(onlinePage.getTenantId(), tokenData.getTenantId())) { + errorMessage = "数据验证失败,当前租户不包含该页面!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(onlinePage); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java new file mode 100644 index 00000000..b5491b5a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java @@ -0,0 +1,175 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.BooleanUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineRuleDto; +import com.orangeforms.common.online.model.OnlineRule; +import com.orangeforms.common.online.service.OnlineRuleService; +import com.orangeforms.common.online.vo.OnlineRuleVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单字段验证规则接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单字段验证规则接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineRule") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineRuleController { + + @Autowired + private OnlineRuleService onlineRuleService; + + /** + * 新增验证规则数据。 + * + * @param onlineRuleDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineRuleDto.ruleId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineRuleDto onlineRuleDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineRuleDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineRule onlineRule = MyModelUtil.copyTo(onlineRuleDto, OnlineRule.class); + onlineRule = onlineRuleService.saveNew(onlineRule); + return ResponseResult.success(onlineRule.getRuleId()); + } + + /** + * 更新验证规则数据。 + * + * @param onlineRuleDto 更新对象。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.UPDATE) + @SaCheckPermission("onlinePage.all") + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineRuleDto onlineRuleDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineRuleDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineRule onlineRule = MyModelUtil.copyTo(onlineRuleDto, OnlineRule.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineRule.getRuleId(), false); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineRule originalOnlineRule = verifyResult.getData(); + if (!onlineRuleService.update(onlineRule, originalOnlineRule)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除验证规则数据。 + * + * @param ruleId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long ruleId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(ruleId, false); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineRuleService.remove(ruleId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的验证规则列表。 + * + * @param onlineRuleDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineRuleDto onlineRuleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineRule onlineRuleFilter = MyModelUtil.copyTo(onlineRuleDtoFilter, OnlineRule.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineRule.class); + List onlineRuleList = onlineRuleService.getOnlineRuleListWithRelation(onlineRuleFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineRuleList, OnlineRuleVo.class)); + } + + /** + * 查看指定验证规则对象详情。 + * + * @param ruleId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long ruleId) { + ResponseResult verifyResult = this.doVerifyAndGet(ruleId, true); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineRule onlineRule = verifyResult.getData(); + return ResponseResult.success(onlineRule, OnlineRuleVo.class); + } + + private ResponseResult doVerifyAndGet(Long ruleId, boolean readOnly) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(ruleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineRule rule = onlineRuleService.getById(ruleId); + if (rule == null) { + errorMessage = "数据验证失败,当前在线字段规则并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!readOnly && BooleanUtil.isTrue(rule.getBuiltin())) { + errorMessage = "数据验证失败,内置规则不能删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(rule.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用并不包含该规则!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(rule); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java new file mode 100644 index 00000000..f28e81d1 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java @@ -0,0 +1,195 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineVirtualColumnDto; +import com.orangeforms.common.online.model.OnlineVirtualColumn; +import com.orangeforms.common.online.model.constant.VirtualType; +import com.orangeforms.common.online.service.OnlineVirtualColumnService; +import com.orangeforms.common.online.vo.OnlineVirtualColumnVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import jakarta.validation.groups.Default; + +/** + * 在线表单虚拟字段接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单虚拟字段接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineVirtualColumn") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineVirtualColumnController { + + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + + /** + * 新增虚拟字段数据。 + * + * @param onlineVirtualColumnDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineVirtualColumnDto.virtualColumnId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineVirtualColumnDto onlineVirtualColumnDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineVirtualColumnDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineVirtualColumn onlineVirtualColumn = + MyModelUtil.copyTo(onlineVirtualColumnDto, OnlineVirtualColumn.class); + ResponseResult verifyResult = this.doVerify(onlineVirtualColumn, null); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + onlineVirtualColumn = onlineVirtualColumnService.saveNew(onlineVirtualColumn); + return ResponseResult.success(onlineVirtualColumn.getVirtualColumnId()); + } + + /** + * 更新虚拟字段数据。 + * + * @param onlineVirtualColumnDto 更新对象。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.UPDATE) + @SaCheckPermission("onlinePage.all") + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineVirtualColumnDto onlineVirtualColumnDto) { + String errorMessage = MyCommonUtil.getModelValidationError( + onlineVirtualColumnDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineVirtualColumn onlineVirtualColumn = + MyModelUtil.copyTo(onlineVirtualColumnDto, OnlineVirtualColumn.class); + OnlineVirtualColumn originalOnlineVirtualColumn = + onlineVirtualColumnService.getById(onlineVirtualColumn.getVirtualColumnId()); + if (originalOnlineVirtualColumn == null) { + errorMessage = "数据验证失败,当前虚拟字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerify(onlineVirtualColumn, originalOnlineVirtualColumn); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineVirtualColumnService.update(onlineVirtualColumn, originalOnlineVirtualColumn)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除虚拟字段数据。 + * + * @param virtualColumnId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.DELETE) + @SaCheckPermission("onlinePage.all") + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long virtualColumnId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(virtualColumnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineVirtualColumn originalOnlineVirtualColumn = onlineVirtualColumnService.getById(virtualColumnId); + if (originalOnlineVirtualColumn == null) { + errorMessage = "数据验证失败,当前虚拟字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineVirtualColumnService.remove(virtualColumnId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的虚拟字段列表。 + * + * @param onlineVirtualColumnDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineVirtualColumnDto onlineVirtualColumnDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineVirtualColumn onlineVirtualColumnFilter = + MyModelUtil.copyTo(onlineVirtualColumnDtoFilter, OnlineVirtualColumn.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineVirtualColumn.class); + List onlineVirtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnListWithRelation(onlineVirtualColumnFilter, orderBy); + MyPageData pageData = + MyPageUtil.makeResponseData(onlineVirtualColumnList, OnlineVirtualColumnVo.class); + return ResponseResult.success(pageData); + } + + /** + * 查看指定虚拟字段对象详情。 + * + * @param virtualColumnId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long virtualColumnId) { + if (MyCommonUtil.existBlankArgument(virtualColumnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineVirtualColumn onlineVirtualColumn = + onlineVirtualColumnService.getByIdWithRelation(virtualColumnId, MyRelationParam.full()); + if (onlineVirtualColumn == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineVirtualColumn, OnlineVirtualColumnVo.class); + } + + private ResponseResult doVerify( + OnlineVirtualColumn virtualColumn, OnlineVirtualColumn originalVirtualColumn) { + if (!virtualColumn.getVirtualType().equals(VirtualType.AGGREGATION)) { + return ResponseResult.success(); + } + if (MyCommonUtil.existBlankArgument( + virtualColumn.getAggregationColumnId(), + virtualColumn.getAggregationTableId(), + virtualColumn.getDatasourceId(), + virtualColumn.getRelationId(), + virtualColumn.getAggregationType())) { + String errorMessage = "数据验证失败,数据源、关联关系、聚合表、聚合字段和聚合类型,均不能为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult verifyResult = onlineVirtualColumnService.verifyRelatedData(virtualColumn, originalVirtualColumn); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java new file mode 100644 index 00000000..fbfc638f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 字段数据数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineColumnMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineColumnFilter 主表过滤对象。 + * @return 对象列表。 + */ + List getOnlineColumnList(@Param("onlineColumnFilter") OnlineColumn onlineColumnFilter); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java new file mode 100644 index 00000000..84128efd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java @@ -0,0 +1,14 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineColumnRule; + +/** + * 数据字段规则访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineColumnRuleMapper extends BaseDaoMapper { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java new file mode 100644 index 00000000..7f5aaca2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java @@ -0,0 +1,60 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDatasource; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 数据模型数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineDatasourceFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDatasourceList( + @Param("onlineDatasourceFilter") OnlineDatasource onlineDatasourceFilter, @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表数据列表。 + * + * @param pageId 关联主表Id。 + * @param onlineDatasourceFilter 从表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 从表数据列表。 + */ + List getOnlineDatasourceListByPageId( + @Param("pageId") Long pageId, + @Param("onlineDatasourceFilter") OnlineDatasource onlineDatasourceFilter, + @Param("orderBy") String orderBy); + + /** + * 根据在线表单Id集合,获取关联的在线数据源对象列表。 + * + * @param formIdSet 在线表单Id集合。 + * @return 与参数表单Id关联的数据源列表。 + */ + List getOnlineDatasourceListByFormIds(@Param("formIdSet") Set formIdSet); + + /** + * 获取在线表单页面和在线表单数据源变量名的映射关系。 + * + * @param pageIds 页面Id集合。 + * @return 在线表单页面和在线表单数据源变量名的映射关系。 + */ + @Select("SELECT a.page_id, b.variable_name FROM zz_online_page_datasource a, zz_online_datasource b" + + " WHERE a.page_id in (${pageIds}) AND a.datasource_id = b.datasource_id") + List> getPageIdAndVariableNameMapByPageIds(@Param("pageIds") String pageIds); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java new file mode 100644 index 00000000..d68c13a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据关联数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceRelationMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDatasourceRelationList( + @Param("filter") OnlineDatasourceRelation filter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java new file mode 100644 index 00000000..a84fbb66 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDatasourceTable; + +/** + * 数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceTableMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java new file mode 100644 index 00000000..1941c7f8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDblink; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据库链接数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDblinkMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineDblinkFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDblinkList( + @Param("onlineDblinkFilter") OnlineDblink onlineDblinkFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java new file mode 100644 index 00000000..b22cca72 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDict; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在线表单字典数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDictMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineDictFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDictList( + @Param("onlineDictFilter") OnlineDict onlineDictFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java new file mode 100644 index 00000000..a8485da4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineFormDatasource; + +/** + * 在线表单与数据源多对多关联的数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineFormDatasourceMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java new file mode 100644 index 00000000..5adbad02 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineForm; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在线表单数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineFormMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineFormFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineFormList( + @Param("onlineFormFilter") OnlineForm onlineFormFilter, @Param("orderBy") String orderBy); + + /** + * 根据数据源Id,返回使用该数据源的OnlineForm对象。 + * + * @param datasourceId 数据源Id。 + * @param onlineFormFilter 主表过滤对象。 + * @return 使用该数据源的表单列表。 + */ + List getOnlineFormListByDatasourceId( + @Param("datasourceId") Long datasourceId, @Param("onlineFormFilter") OnlineForm onlineFormFilter); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java new file mode 100644 index 00000000..025e437c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java @@ -0,0 +1,259 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.object.ColumnData; +import com.orangeforms.common.online.object.JoinTableInfo; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; + +/** + * 在线表单运行时数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Mapper +public interface OnlineOperationMapper { + + /** + * 插入新数据。 + * + * @param tableName 数据表名。 + * @param columnNames 字段名列表。 + * @param columnValueList 字段值列表。 + */ + @Insert("") + void insert( + @Param("tableName") String tableName, + @Param("columnNames") String columnNames, + @Param("columnValueList") List columnValueList); + + /** + * 更新表数据。 + * + * @param tableName 数据表名。 + * @param updateColumnList 更新字段列表。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @return 更新行数。 + */ + @Update("") + int update( + @Param("tableName") String tableName, + @Param("updateColumnList") List updateColumnList, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter); + + /** + * 删除指定数据。 + * + * @param tableName 表名。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @return 删除行数。 + */ + @Delete("") + int delete( + @Param("tableName") String tableName, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter); + + /** + * 执行动态查询,并返回查询结果集。 + * + * @param masterTableName 主表名称。 + * @param joinInfoList 关联表信息列表。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @param orderBy 排序字符串。 + * @return 查询结果集。 + */ + @Select("") + List> getList( + @Param("masterTableName") String masterTableName, + @Param("joinInfoList") List joinInfoList, + @Param("selectFields") String selectFields, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter, + @Param("orderBy") String orderBy); + + /** + * 以字典键值对的方式返回数据。 + * + * @param tableName 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @return 查询结果集。 + */ + @Select("") + List> getDictList( + @Param("tableName") String tableName, + @Param("selectFields") String selectFields, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter); + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy 分组字段列表,逗号分隔。 + * @return 对象可选字段Map列表。 + */ + @Select("") + List> getGroupedListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("groupBy") String groupBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java new file mode 100644 index 00000000..d486645d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlinePageDatasource; + +/** + * 在线表单页面和数据源关联对象的数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlinePageDatasourceMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java new file mode 100644 index 00000000..7ac0841f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlinePage; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在线表单页面数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlinePageMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlinePageFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlinePageList( + @Param("onlinePageFilter") OnlinePage onlinePageFilter, @Param("orderBy") String orderBy); + + /** + /** + * 根据数据源Id,返回使用该数据源的OnlinePage对象。 + * + * @param datasourceId 数据源Id。 + * @return 使用该数据源的页面列表。 + */ + List getOnlinePageListByDatasourceId( + @Param("datasourceId") Long datasourceId, @Param("onlinePageFilter") OnlinePage onlinePageFilter); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java new file mode 100644 index 00000000..245ba10b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineRule; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 验证规则数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineRuleMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineRuleFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineRuleList( + @Param("onlineRuleFilter") OnlineRule onlineRuleFilter, @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表数据列表。 + * + * @param columnId 关联主表Id。 + * @param onlineRuleFilter 从表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 从表数据列表。 + */ + List getOnlineRuleListByColumnId( + @Param("columnId") Long columnId, + @Param("onlineRuleFilter") OnlineRule onlineRuleFilter, + @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表中没有和主表建立关联关系的数据列表。 + * + * @param columnId 关联主表Id。 + * @param onlineRuleFilter 过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 与主表没有建立关联的从表数据列表。 + */ + List getNotInOnlineRuleListByColumnId( + @Param("columnId") Long columnId, + @Param("onlineRuleFilter") OnlineRule onlineRuleFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java new file mode 100644 index 00000000..238c0bae --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineTable; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据表数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineTableMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineTableFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineTableList( + @Param("onlineTableFilter") OnlineTable onlineTableFilter, @Param("orderBy") String orderBy); + + /** + * 根据数据源Id,获取该数据源及其关联所引用的数据表列表。 + * + * @param datasourceId 指定的数据源Id。 + * @return 该数据源及其关联所引用的数据表列表。 + */ + List getOnlineTableListByDatasourceId(@Param("datasourceId") Long datasourceId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java new file mode 100644 index 00000000..78ca3d20 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineVirtualColumn; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 虚拟字段数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineVirtualColumnMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineVirtualColumnFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineVirtualColumnList( + @Param("onlineVirtualColumnFilter") OnlineVirtualColumn onlineVirtualColumnFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml new file mode 100644 index 00000000..ede95b2e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_column.table_id = #{onlineColumnFilter.tableId} + + + AND zz_online_column.column_name = #{onlineColumnFilter.columnName} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml new file mode 100644 index 00000000..c5afda31 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml new file mode 100644 index 00000000..b148a15b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_datasource.app_code IS NULL + + + AND zz_online_datasource.app_code = #{onlineDatasourceFilter.appCode} + + + AND zz_online_datasource.variable_name = #{onlineDatasourceFilter.variableName} + + + AND zz_online_datasource.datasource_name = #{onlineDatasourceFilter.datasourceName} + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml new file mode 100644 index 00000000..c669d3d2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_datasource_relation.app_code IS NULL + + + AND zz_online_datasource_relation.app_code = #{filter.appCode} + + + AND zz_online_datasource_relation.relation_name = #{filter.relationName} + + + AND zz_online_datasource_relation.datasource_id = #{filter.datasourceId} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml new file mode 100644 index 00000000..d3ba6aaa --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml new file mode 100644 index 00000000..59f94b1e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_dblink.app_code IS NULL + + + AND zz_online_dblink.app_code = #{onlineDblinkFilter.appCode} + + + AND zz_online_dblink.dblink_type = #{onlineDblinkFilter.dblinkType} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml new file mode 100644 index 00000000..cf1fa27e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_dict.dict_id = #{onlineDictFilter.dictId} + + + AND zz_online_dict.app_code IS NULL + + + AND zz_online_dict.app_code = #{onlineDictFilter.appCode} + + + AND zz_online_dict.dict_name = #{onlineDictFilter.dictName} + + + AND zz_online_dict.dict_type = #{onlineDictFilter.dictType} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml new file mode 100644 index 00000000..5d0924ff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml new file mode 100644 index 00000000..a79415be --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_form.tenant_id IS NULL + + + AND zz_online_form.tenant_id = #{onlineFormFilter.tenantId} + + + AND zz_online_form.app_code IS NULL + + + AND zz_online_form.app_code = #{onlineFormFilter.appCode} + + + AND zz_online_form.page_id = #{onlineFormFilter.pageId} + + + AND zz_online_form.form_code = #{onlineFormFilter.formCode} + + + + AND zz_online_form.form_name LIKE #{safeFormName} + + + AND zz_online_form.form_type = #{onlineFormFilter.formType} + + + AND zz_online_form.master_table_id = #{onlineFormFilter.masterTableId} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml new file mode 100644 index 00000000..47d8b88d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml new file mode 100644 index 00000000..86aeeb21 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_page.tenant_id IS NULL + + + AND zz_online_page.tenant_id = #{onlinePageFilter.tenantId} + + + AND zz_online_page.app_code IS NULL + + + AND zz_online_page.app_code = #{onlinePageFilter.appCode} + + + AND zz_online_page.page_code = #{onlinePageFilter.pageCode} + + + + AND zz_online_page.page_name LIKE #{safePageName} + + + AND zz_online_page.page_type = #{onlinePageFilter.pageType} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml new file mode 100644 index 00000000..35095622 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + AND (zz_online_rule.app_code IS NULL OR zz_online_rule.builtin = 1) + + + AND (zz_online_rule.app_code = #{onlineRuleFilter.appCode} OR zz_online_rule.builtin = 1) + + + AND zz_online_rule.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml new file mode 100644 index 00000000..abb2569b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_table.app_code IS NULL + + + AND zz_online_table.app_code = #{onlineTableFilter.appCode} + + + AND zz_online_table.table_name = #{onlineTableFilter.tableName} + + + AND zz_online_table.model_name = #{onlineTableFilter.modelName} + + + AND zz_online_table.dblink_id = #{onlineTableFilter.dblinkId} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml new file mode 100644 index 00000000..1dbc69e8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_virtual_column.datasource_id = #{onlineVirtualColumnFilter.datasourceId} + + + AND zz_online_virtual_column.relation_id = #{onlineVirtualColumnFilter.relationId} + + + AND zz_online_virtual_column.table_id = #{onlineVirtualColumnFilter.tableId} + + + AND zz_online_virtual_column.aggregation_column_id = #{onlineVirtualColumnFilter.aggregationColumnId} + + + AND zz_online_virtual_column.virtual_type = #{onlineVirtualColumnFilter.virtualType} + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java new file mode 100644 index 00000000..a3713cbf --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java @@ -0,0 +1,189 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.model.constant.FieldKind; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表字段Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段Dto对象") +@Data +public class OnlineColumnDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long columnId; + + /** + * 字段名。 + */ + @Schema(description = "字段名") + @NotBlank(message = "数据验证失败,字段名不能为空!") + private String columnName; + + /** + * 数据表Id。 + */ + @Schema(description = "数据表Id") + @NotNull(message = "数据验证失败,数据表Id不能为空!") + private Long tableId; + + /** + * 数据表中的字段类型。 + */ + @Schema(description = "数据表中的字段类型") + @NotBlank(message = "数据验证失败,数据表中的字段类型不能为空!") + private String columnType; + + /** + * 数据表中的完整字段类型(包括了精度和刻度)。 + */ + @Schema(description = "数据表中的完整字段类型") + @NotBlank(message = "数据验证失败,数据表中的完整字段类型(包括了精度和刻度)不能为空!") + private String fullColumnType; + + /** + * 是否为主键。 + */ + @Schema(description = "是否为主键") + @NotNull(message = "数据验证失败,是否为主键不能为空!") + private Boolean primaryKey; + + /** + * 是否是自增主键(0: 不是 1: 是)。 + */ + @Schema(description = "是否是自增主键") + @NotNull(message = "数据验证失败,是否是自增主键(0: 不是 1: 是)不能为空!") + private Boolean autoIncrement; + + /** + * 是否可以为空 (0: 不可以为空 1: 可以为空)。 + */ + @Schema(description = "是否可以为空") + @NotNull(message = "数据验证失败,是否可以为空 (0: 不可以为空 1: 可以为空)不能为空!") + private Boolean nullable; + + /** + * 缺省值。 + */ + @Schema(description = "缺省值") + private String columnDefault; + + /** + * 字段在数据表中的显示位置。 + */ + @Schema(description = "字段在数据表中的显示位置") + @NotNull(message = "数据验证失败,字段在数据表中的显示位置不能为空!") + private Integer columnShowOrder; + + /** + * 数据表中的字段注释。 + */ + @Schema(description = "数据表中的字段注释") + private String columnComment; + + /** + * 对象映射字段名称。 + */ + @Schema(description = "对象映射字段名称") + @NotBlank(message = "数据验证失败,对象映射字段名称不能为空!") + private String objectFieldName; + + /** + * 对象映射字段类型。 + */ + @Schema(description = "对象映射字段类型") + @NotBlank(message = "数据验证失败,对象映射字段类型不能为空!") + private String objectFieldType; + + /** + * 数值型字段的精度(目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的精度") + private Integer numericPrecision; + + /** + * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的刻度") + private Integer numericScale; + + /** + * 过滤类型字段。 + */ + @Schema(description = "过滤类型字段") + @NotNull(message = "数据验证失败,过滤类型字段不能为空!", groups = {UpdateGroup.class}) + @ConstDictRef(constDictClass = FieldFilterType.class, message = "数据验证失败,过滤类型字段为无效值!") + private Integer filterType; + + /** + * 是否是主键的父Id。 + */ + @Schema(description = "是否是主键的父Id") + @NotNull(message = "数据验证失败,是否是主键的父Id不能为空!") + private Boolean parentKey; + + /** + * 是否部门过滤字段。 + */ + @Schema(description = "是否部门过滤字段") + @NotNull(message = "数据验证失败,是否部门过滤字段标记不能为空!") + private Boolean deptFilter; + + /** + * 是否用户过滤字段。 + */ + @Schema(description = "是否用户过滤字段") + @NotNull(message = "数据验证失败,是否用户过滤字段标记不能为空!") + private Boolean userFilter; + + /** + * 字段类别。 + */ + @Schema(description = "字段类别") + @ConstDictRef(constDictClass = FieldKind.class, message = "数据验证失败,字段类别为无效值!") + private Integer fieldKind; + + /** + * 包含的文件文件数量,0表示无限制。 + */ + @Schema(description = "包含的文件文件数量,0表示无限制") + private Integer maxFileCount; + + /** + * 上传文件系统类型。 + */ + @Schema(description = "上传文件系统类型") + private Integer uploadFileSystemType; + + /** + * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 + */ + @Schema(description = "脱敏字段类型") + private String maskFieldType; + + /** + * 编码规则的JSON格式数据。 + */ + @Schema(description = "编码规则的JSON格式数据") + private String encodedRule; + + /** + * 字典Id。 + */ + @Schema(description = "字典Id") + private Long dictId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java new file mode 100644 index 00000000..d6789157 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表字段规则和字段多对多关联Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段规则和字段多对多关联Dto对象") +@Data +public class OnlineColumnRuleDto { + + /** + * 字段Id。 + */ + @Schema(description = "字段Id") + @NotNull(message = "数据验证失败,字段Id不能为空!", groups = {UpdateGroup.class}) + private Long columnId; + + /** + * 规则Id。 + */ + @Schema(description = "规则Id") + @NotNull(message = "数据验证失败,规则Id不能为空!", groups = {UpdateGroup.class}) + private Long ruleId; + + /** + * 规则属性数据。 + */ + @Schema(description = "规则属性数据") + private String propDataJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java new file mode 100644 index 00000000..0fbb006d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单的数据源Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源Dto对象") +@Data +public class OnlineDatasourceDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long datasourceId; + + /** + * 数据源名称。 + */ + @Schema(description = "数据源名称") + @NotBlank(message = "数据验证失败,数据源名称不能为空!") + private String datasourceName; + + /** + * 数据源变量名,会成为数据访问url的一部分。 + */ + @Schema(description = "数据源变量名,会成为数据访问url的一部分") + @NotBlank(message = "数据验证失败,数据源变量名不能为空!") + private String variableName; + + /** + * 主表所在的数据库链接Id。 + */ + @Schema(description = "主表所在的数据库链接Id") + @NotNull(message = "数据验证失败,数据库链接Id不能为空!") + private Long dblinkId; + + /** + * 主表Id。 + */ + @Schema(description = "主表Id") + @NotNull(message = "数据验证失败,主表Id不能为空!", groups = {UpdateGroup.class}) + private Long masterTableId; + + /** + * 主表表名。 + */ + @Schema(description = "主表表名") + @NotBlank(message = "数据验证失败,主表名不能为空!", groups = {AddGroup.class}) + private String masterTableName; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java new file mode 100644 index 00000000..3ad19465 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java @@ -0,0 +1,107 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.RelationType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单的数据源关联Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源关联Dto对象") +@Data +public class OnlineDatasourceRelationDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long relationId; + + /** + * 关联名称。 + */ + @Schema(description = "关联名称") + @NotBlank(message = "数据验证失败,关联名称不能为空!") + private String relationName; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + @NotBlank(message = "数据验证失败,变量名不能为空!") + private String variableName; + + /** + * 主数据源Id。 + */ + @Schema(description = "主数据源Id") + @NotNull(message = "数据验证失败,主数据源Id不能为空!") + private Long datasourceId; + + /** + * 关联类型。 + */ + @Schema(description = "关联类型") + @NotNull(message = "数据验证失败,关联类型不能为空!") + @ConstDictRef(constDictClass = RelationType.class, message = "数据验证失败,关联类型为无效值!") + private Integer relationType; + + /** + * 主表关联字段Id。 + */ + @Schema(description = "主表关联字段Id") + @NotNull(message = "数据验证失败,主表关联字段Id不能为空!") + private Long masterColumnId; + + /** + * 从表Id。 + */ + @Schema(description = "从表Id") + @NotNull(message = "数据验证失败,从表Id不能为空!", groups = {UpdateGroup.class}) + private Long slaveTableId; + + /** + * 从表名。 + */ + @Schema(description = "从表名") + @NotBlank(message = "数据验证失败,从表名不能为空!", groups = {AddGroup.class}) + private String slaveTableName; + + /** + * 从表关联字段Id。 + */ + @Schema(description = "从表关联字段Id") + @NotNull(message = "数据验证失败,从表关联字段Id不能为空!", groups = {UpdateGroup.class}) + private Long slaveColumnId; + + /** + * 从表字段名。 + */ + @Schema(description = "从表字段名") + @NotBlank(message = "数据验证失败,从表字段名不能为空!", groups = {AddGroup.class}) + private String slaveColumnName; + + /** + * 是否级联删除标记。 + */ + @Schema(description = "是否级联删除标记") + @NotNull(message = "数据验证失败,是否级联删除标记不能为空!") + private Boolean cascadeDelete; + + /** + * 是否左连接标记。 + */ + @Schema(description = "是否左连接标记") + @NotNull(message = "数据验证失败,是否左连接标记不能为空!") + private Boolean leftJoin; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java new file mode 100644 index 00000000..2e1f2488 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java @@ -0,0 +1,53 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表所在数据库链接Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表所在数据库链接Dto对象") +@Data +public class OnlineDblinkDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long dblinkId; + + /** + * 链接中文名称。 + */ + @Schema(description = "链接中文名称") + @NotBlank(message = "数据验证失败,链接中文名称不能为空!") + private String dblinkName; + + /** + * 链接描述。 + */ + @Schema(description = "链接中文名称") + private String dblinkDescription; + + /** + * 配置信息。 + */ + @Schema(description = "配置信息") + @NotBlank(message = "数据验证失败,配置信息不能为空!") + private String configuration; + + /** + * 数据库链接类型。 + */ + @Schema(description = "数据库链接类型") + @NotNull(message = "数据验证失败,数据库链接类型不能为空!") + private Integer dblinkType; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java new file mode 100644 index 00000000..f25444ce --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java @@ -0,0 +1,128 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.constant.DictType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单关联的字典Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单关联的字典Dto对象") +@Data +public class OnlineDictDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long dictId; + + /** + * 字典名称。 + */ + @Schema(description = "字典名称") + @NotBlank(message = "数据验证失败,字典名称不能为空!") + private String dictName; + + /** + * 字典类型。 + */ + @Schema(description = "字典类型") + @NotNull(message = "数据验证失败,字典类型不能为空!") + @ConstDictRef(constDictClass = DictType.class, message = "数据验证失败,字典类型为无效值!") + private Integer dictType; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 字典表名称。 + */ + @Schema(description = "字典表名称") + private String tableName; + + /** + * 全局字典编码。 + */ + @Schema(description = "全局字典编码") + private String dictCode; + + /** + * 字典表键字段名称。 + */ + @Schema(description = "字典表键字段名称") + private String keyColumnName; + + /** + * 字典表父键字段名称。 + */ + @Schema(description = "字典表父键字段名称") + private String parentKeyColumnName; + + /** + * 字典值字段名称。 + */ + @Schema(description = "字典值字段名称") + private String valueColumnName; + + /** + * 逻辑删除字段。 + */ + @Schema(description = "逻辑删除字段") + private String deletedColumnName; + + /** + * 用户过滤滤字段名称。 + */ + @Schema(description = "用户过滤滤字段名称") + private String userFilterColumnName; + + /** + * 部门过滤字段名称。 + */ + @Schema(description = "部门过滤字段名称") + private String deptFilterColumnName; + + /** + * 租户过滤字段名称。 + */ + @Schema(description = "租户过滤字段名称") + private String tenantFilterColumnName; + + /** + * 获取字典数据的url。 + */ + @Schema(description = "获取字典数据的url") + private String dictListUrl; + + /** + * 根据主键id批量获取字典数据的url。 + */ + @Schema(description = "根据主键id批量获取字典数据的url") + private String dictIdsUrl; + + /** + * 字典的JSON数据。 + */ + @Schema(description = "字典的JSON数据") + private String dictDataJson; + + /** + * 是否树形标记。 + */ + @Schema(description = "是否树形标记") + @NotNull(message = "数据验证失败,是否树形标记不能为空!") + private Boolean treeFlag; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java new file mode 100644 index 00000000..8d638b90 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java @@ -0,0 +1,72 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.online.model.constant.FieldFilterType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Set; + +/** + * 在线表单数据过滤参数对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据过滤参数对象") +@Data +public class OnlineFilterDto { + + /** + * 表名。 + */ + @Schema(description = "表名") + private String tableName; + + /** + * 过滤字段名。 + */ + @Schema(description = "过滤字段名") + private String columnName; + + /** + * 过滤值。 + */ + @Schema(description = "过滤值") + private Object columnValue; + + /** + * 范围比较的最小值。 + */ + @Schema(description = "范围比较的最小值") + private Object columnValueStart; + + /** + * 范围比较的最大值。 + */ + @Schema(description = "范围比较的最大值") + private Object columnValueEnd; + + /** + * 仅当操作符为IN的时候使用。 + */ + @Schema(description = "仅当操作符为IN的时候使用") + private Set columnValueList; + + /** + * 过滤类型,参考FieldFilterType常量对象。缺省值就是等于过滤了。 + */ + @Schema(description = "过滤类型") + private Integer filterType = FieldFilterType.EQUAL_FILTER; + + /** + * 是否为字典多选。 + */ + @Schema(description = "是否为字典多选") + private Boolean dictMultiSelect = false; + + /** + * 是否为Oracle的日期类型。 + */ + private Boolean isOracleDate = false; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java new file mode 100644 index 00000000..2abcde8c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java @@ -0,0 +1,91 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.FormKind; +import com.orangeforms.common.online.model.constant.FormType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * 在线表单Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单Dto对象") +@Data +public class OnlineFormDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long formId; + + /** + * 页面id。 + */ + @Schema(description = "页面id") + @NotNull(message = "数据验证失败,页面id不能为空!") + private Long pageId; + + /** + * 表单编码。 + */ + @Schema(description = "表单编码") + private String formCode; + + /** + * 表单名称。 + */ + @Schema(description = "表单名称") + @NotBlank(message = "数据验证失败,表单名称不能为空!") + private String formName; + + /** + * 表单类别。 + */ + @Schema(description = "表单类别") + @NotNull(message = "数据验证失败,表单类别不能为空!") + @ConstDictRef(constDictClass = FormKind.class, message = "数据验证失败,表单类别为无效值!") + private Integer formKind; + + /** + * 表单类型。 + */ + @Schema(description = "表单类型") + @NotNull(message = "数据验证失败,表单类型不能为空!") + @ConstDictRef(constDictClass = FormType.class, message = "数据验证失败,表单类型为无效值!") + private Integer formType; + + /** + * 表单主表id。 + */ + @Schema(description = "表单主表id") + @NotNull(message = "数据验证失败,表单主表id不能为空!") + private Long masterTableId; + + /** + * 当前表单关联的数据源Id集合。 + */ + @Schema(description = "当前表单关联的数据源Id集合") + private List datasourceIdList; + + /** + * 表单组件JSON。 + */ + @Schema(description = "表单组件JSON") + private String widgetJson; + + /** + * 表单参数JSON。 + */ + @Schema(description = "表单参数JSON") + private String paramsJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java new file mode 100644 index 00000000..e6a3c3c3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单页面和数据源多对多关联Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单页面和数据源多对多关联Dto对象") +@Data +public class OnlinePageDatasourceDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long id; + + /** + * 页面主键Id。 + */ + @Schema(description = "页面主键Id") + @NotNull(message = "数据验证失败,页面主键Id不能为空!") + private Long pageId; + + /** + * 数据源主键Id。 + */ + @Schema(description = "数据源主键Id") + @NotNull(message = "数据验证失败,数据源主键Id不能为空!") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java new file mode 100644 index 00000000..309c3bf4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.model.constant.PageType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单所在页面Dto对象。这里我们可以把页面理解为表单的容器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单所在页面Dto对象") +@Data +public class OnlinePageDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long pageId; + + /** + * 页面编码。 + */ + @Schema(description = "页面编码") + private String pageCode; + + /** + * 页面名称。 + */ + @Schema(description = "页面名称") + @NotBlank(message = "数据验证失败,页面名称不能为空!") + private String pageName; + + /** + * 页面类型。 + */ + @Schema(description = "页面类型") + @NotNull(message = "数据验证失败,页面类型不能为空!") + @ConstDictRef(constDictClass = PageType.class, message = "数据验证失败,页面类型为无效值!") + private Integer pageType; + + /** + * 页面编辑状态。 + */ + @Schema(description = "页面编辑状态") + @NotNull(message = "数据验证失败,状态不能为空!") + @ConstDictRef(constDictClass = PageStatus.class, message = "数据验证失败,状态为无效值!") + private Integer status; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java new file mode 100644 index 00000000..e89517c0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java @@ -0,0 +1,56 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.RuleType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表字段验证规则Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段验证规则Dto对象") +@Data +public class OnlineRuleDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long ruleId; + + /** + * 规则名称。 + */ + @Schema(description = "规则名称") + @NotBlank(message = "数据验证失败,规则名称不能为空!") + private String ruleName; + + /** + * 规则类型。 + */ + @Schema(description = "规则类型") + @NotNull(message = "数据验证失败,规则类型不能为空!") + @ConstDictRef(constDictClass = RuleType.class, message = "数据验证失败,规则类型为无效值!") + private Integer ruleType; + + /** + * 内置规则标记。 + */ + @Schema(description = "内置规则标记") + @NotNull(message = "数据验证失败,内置规则标记不能为空!") + private Boolean builtin; + + /** + * 自定义规则的正则表达式。 + */ + @Schema(description = "自定义规则的正则表达式") + private String pattern; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java new file mode 100644 index 00000000..774f985b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单的数据表Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据表Dto对象") +@Data +public class OnlineTableDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long tableId; + + /** + * 表名称。 + */ + @Schema(description = "表名称") + @NotBlank(message = "数据验证失败,表名称不能为空!") + private String tableName; + + /** + * 实体名称。 + */ + @Schema(description = "实体名称") + @NotBlank(message = "数据验证失败,实体名称不能为空!") + private String modelName; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + @NotNull(message = "数据验证失败,数据库链接Id不能为空!") + private Long dblinkId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java new file mode 100644 index 00000000..040850de --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java @@ -0,0 +1,102 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.constant.AggregationType; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; + +import com.orangeforms.common.online.model.constant.VirtualType; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 在线数据表虚拟字段Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线数据表虚拟字段Dto对象") +@Data +public class OnlineVirtualColumnDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long virtualColumnId; + + /** + * 所在表Id。 + */ + @Schema(description = "所在表Id") + private Long tableId; + + /** + * 字段名称。 + */ + @Schema(description = "字段名称") + @NotBlank(message = "数据验证失败,字段名称不能为空!") + private String objectFieldName; + + /** + * 属性类型。 + */ + @Schema(description = "属性类型") + @NotBlank(message = "数据验证失败,属性类型不能为空!") + private String objectFieldType; + + /** + * 字段提示名。 + */ + @Schema(description = "字段提示名") + @NotBlank(message = "数据验证失败,字段提示名不能为空!") + private String columnPrompt; + + /** + * 虚拟字段类型(0: 聚合)。 + */ + @Schema(description = "虚拟字段类型(0: 聚合)") + @ConstDictRef(constDictClass = VirtualType.class, message = "数据验证失败,虚拟字段类型为无效值!") + @NotNull(message = "数据验证失败,虚拟字段类型(0: 聚合)不能为空!") + private Integer virtualType; + + /** + * 关联数据源Id。 + */ + @Schema(description = "关联数据源Id") + @NotNull(message = "数据验证失败,关联数据源Id不能为空!") + private Long datasourceId; + + /** + * 关联Id。 + */ + @Schema(description = "关联Id") + private Long relationId; + + /** + * 聚合字段所在关联表Id。 + */ + @Schema(description = "聚合字段所在关联表Id") + private Long aggregationTableId; + + /** + * 关联表聚合字段Id。 + */ + @Schema(description = "关联表聚合字段Id") + private Long aggregationColumnId; + + /** + * 聚合类型(0: sum 1: count 2: avg 3: min 4: max)。 + */ + @Schema(description = "聚合类型(0: sum 1: count 2: avg 3: min 4: max)") + @ConstDictRef(constDictClass = AggregationType.class, message = "数据验证失败,虚拟字段聚合计算类型为无效值!") + private Integer aggregationType; + + /** + * 存储过滤条件的json。 + */ + @Schema(description = "存储过滤条件的json") + private String whereClauseJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java new file mode 100644 index 00000000..a2ac52f2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.online.exception; + +import com.orangeforms.common.core.exception.MyRuntimeException; + +/** + * 在线表单运行时异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineRuntimeException extends MyRuntimeException { + + /** + * 构造函数。 + */ + public OnlineRuntimeException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public OnlineRuntimeException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java new file mode 100644 index 00000000..0c1d2cff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java @@ -0,0 +1,215 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationOneToOne; +import com.orangeforms.common.online.model.constant.FieldKind; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_column") +public class OnlineColumn { + + /** + * 主键Id。 + */ + @Id(value = "column_id") + private Long columnId; + + /** + * 字段名。 + */ + @Column(value = "column_name") + private String columnName; + + /** + * 数据表Id。 + */ + @Column(value = "table_id") + private Long tableId; + + /** + * 数据表中的字段类型。 + */ + @Column(value = "column_type") + private String columnType; + + /** + * 数据表中的完整字段类型(包括了精度和刻度)。 + */ + @Column(value = "full_column_type") + private String fullColumnType; + + /** + * 是否为主键。 + */ + @Column(value = "primary_key") + private Boolean primaryKey; + + /** + * 是否是自增主键(0: 不是 1: 是)。 + */ + @Column(value = "auto_incr") + private Boolean autoIncrement; + + /** + * 是否可以为空 (0: 不可以为空 1: 可以为空)。 + */ + @Column(value = "nullable") + private Boolean nullable; + + /** + * 缺省值。 + */ + @Column(value = "column_default") + private String columnDefault; + + /** + * 字段在数据表中的显示位置。 + */ + @Column(value = "column_show_order") + private Integer columnShowOrder; + + /** + * 数据表中的字段注释。 + */ + @Column(value = "column_comment") + private String columnComment; + + /** + * 对象映射字段名称。 + */ + @Column(value = "object_field_name") + private String objectFieldName; + + /** + * 对象映射字段类型。 + */ + @Column(value = "object_field_type") + private String objectFieldType; + + /** + * 数值型字段的精度(目前仅Oracle使用)。 + */ + @Column(value = "numeric_precision") + private Integer numericPrecision; + + /** + * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 + */ + @Column(value = "numeric_scale") + private Integer numericScale; + + /** + * 过滤字段类型。 + */ + @Column(value = "filter_type") + private Integer filterType; + + /** + * 是否是主键的父Id。 + */ + @Column(value = "parent_key") + private Boolean parentKey; + + /** + * 是否部门过滤字段。 + */ + @Column(value = "dept_filter") + private Boolean deptFilter; + + /** + * 是否用户过滤字段。 + */ + @Column(value = "user_filter") + private Boolean userFilter; + + /** + * 字段类别。 + */ + @Column(value = "field_kind") + private Integer fieldKind; + + /** + * 包含的文件文件数量,0表示无限制。 + */ + @Column(value = "max_file_count") + private Integer maxFileCount; + + /** + * 上传文件系统类型。 + */ + @Column(value = "upload_file_system_type") + private Integer uploadFileSystemType; + + /** + * 编码规则的JSON格式数据。 + */ + @Column(value = "encoded_rule") + private String encodedRule; + + /** + * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 + */ + @Column(value = "mask_field_type") + private String maskFieldType; + + /** + * 字典Id。 + */ + @Column(value = "dict_id") + private Long dictId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * SQL查询时候使用的别名。 + */ + @Column(ignore = true) + private String columnAliasName; + + @RelationConstDict( + masterIdField = "fieldKind", + constantDictClass = FieldKind.class) + @Column(ignore = true) + private Map fieldKindDictMap; + + @RelationOneToOne( + masterIdField = "dictId", + slaveModelClass = OnlineDict.class, + slaveIdField = "dictId", + loadSlaveDict = false) + @Column(ignore = true) + private OnlineDict dictInfo; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java new file mode 100644 index 00000000..f04517af --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 在线表单数据表字段规则和字段多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_column_rule") +public class OnlineColumnRule { + + /** + * 字段Id。 + */ + @Column(value = "column_id") + private Long columnId; + + /** + * 规则Id。 + */ + @Column(value = "rule_id") + private Long ruleId; + + /** + * 规则属性数据。 + */ + @Column(value = "prop_data_json") + private String propDataJson; + + @Column(ignore = true) + private OnlineRule onlineRule; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java new file mode 100644 index 00000000..2ac6c3a7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java @@ -0,0 +1,103 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationDict; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单的数据源实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_datasource") +public class OnlineDatasource { + + /** + * 主键Id。 + */ + @Id(value = "datasource_id") + private Long datasourceId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 数据源名称。 + */ + @Column(value = "datasource_name") + private String datasourceName; + + /** + * 数据源变量名,会成为数据访问url的一部分。 + */ + @Column(value = "variable_name") + private String variableName; + + /** + * 数据库链接Id。 + */ + @Column(value = "dblink_id") + private Long dblinkId; + + /** + * 主表Id。 + */ + @Column(value = "master_table_id") + private Long masterTableId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * datasourceId 的多对多关联的数据对象。 + */ + @Column(ignore = true) + private OnlinePageDatasource onlinePageDatasource; + + /** + * datasourceId 的多对多关联的数据对象。 + */ + @Column(ignore = true) + private List onlineFormDatasourceList; + + @RelationDict( + masterIdField = "masterTableId", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId", + slaveNameField = "tableName") + @Column(ignore = true) + private Map masterTableIdDictMap; + + @Column(ignore = true) + private OnlineTable masterTable; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java new file mode 100644 index 00000000..be965365 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java @@ -0,0 +1,166 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationDict; +import com.orangeforms.common.core.annotation.RelationOneToOne; +import com.orangeforms.common.online.model.constant.RelationType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单的数据源关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_datasource_relation") +public class OnlineDatasourceRelation { + + /** + * 主键Id。 + */ + @Id(value = "relation_id") + private Long relationId; + + /** + * 应用Id。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 关联名称。 + */ + @Column(value = "relation_name") + private String relationName; + + /** + * 变量名。 + */ + @Column(value = "variable_name") + private String variableName; + + /** + * 主数据源Id。 + */ + @Column(value = "datasource_id") + private Long datasourceId; + + /** + * 关联类型。 + */ + @Column(value = "relation_type") + private Integer relationType; + + /** + * 主表关联字段Id。 + */ + @Column(value = "master_column_id") + private Long masterColumnId; + + /** + * 从表Id。 + */ + @Column(value = "slave_table_id") + private Long slaveTableId; + + /** + * 从表关联字段Id。 + */ + @Column(value = "slave_column_id") + private Long slaveColumnId; + + /** + * 删除主表的时候是否级联删除一对一和一对多的从表数据,多对多只是删除关联,不受到这个标记的影响。。 + */ + @Column(value = "cascade_delete") + private Boolean cascadeDelete; + + /** + * 是否左连接。 + */ + @Column(value = "left_join") + private Boolean leftJoin; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + @RelationOneToOne( + masterIdField = "masterColumnId", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId") + @Column(ignore = true) + private OnlineColumn masterColumn; + + @RelationOneToOne( + masterIdField = "slaveTableId", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId") + @Column(ignore = true) + private OnlineTable slaveTable; + + @RelationOneToOne( + masterIdField = "slaveColumnId", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId") + @Column(ignore = true) + private OnlineColumn slaveColumn; + + @RelationDict( + masterIdField = "masterColumnId", + equalOneToOneRelationField = "onlineColumn", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId", + slaveNameField = "columnName") + @Column(ignore = true) + private Map masterColumnIdDictMap; + + @RelationDict( + masterIdField = "slaveTableId", + equalOneToOneRelationField = "onlineTable", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId", + slaveNameField = "modelName") + @Column(ignore = true) + private Map slaveTableIdDictMap; + + @RelationDict( + masterIdField = "slaveColumnId", + equalOneToOneRelationField = "onlineColumn", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId", + slaveNameField = "columnName") + @Column(ignore = true) + private Map slaveColumnIdDictMap; + + @RelationConstDict( + masterIdField = "relationType", + constantDictClass = RelationType.class) + @Column(ignore = true) + private Map relationTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java new file mode 100644 index 00000000..c7bf696f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 数据源及其关联所引用的数据表实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_datasource_table") +public class OnlineDatasourceTable { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 数据源Id。 + */ + @Column(value = "datasource_id") + private Long datasourceId; + + /** + * 数据源关联Id。 + */ + @Column(value = "relation_id") + private Long relationId; + + /** + * 数据表Id。 + */ + @Column(value = "table_id") + private Long tableId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java new file mode 100644 index 00000000..343b6bec --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java @@ -0,0 +1,85 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.dbutil.constant.DblinkType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表所在数据库链接实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_dblink") +public class OnlineDblink { + + /** + * 主键Id。 + */ + @Id(value = "dblink_id") + private Long dblinkId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 链接中文名称。 + */ + @Column(value = "dblink_name") + private String dblinkName; + + /** + * 链接描述。 + */ + @Column(value = "dblink_description") + private String dblinkDescription; + + /** + * 配置信息。 + */ + private String configuration; + + /** + * 数据库链接类型。 + */ + @Column(value = "dblink_type") + private Integer dblinkType; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 修改时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + @RelationConstDict( + masterIdField = "dblinkType", + constantDictClass = DblinkType.class) + @Column(ignore = true) + private Map dblinkTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java new file mode 100644 index 00000000..c93d97ec --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java @@ -0,0 +1,167 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationDict; +import com.orangeforms.common.core.constant.DictType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单关联的字典实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_dict") +public class OnlineDict { + + /** + * 主键Id。 + */ + @Id(value = "dict_id") + private Long dictId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 字典名称。 + */ + @Column(value = "dict_name") + private String dictName; + + /** + * 字典类型。 + */ + @Column(value = "dict_type") + private Integer dictType; + + /** + * 数据库链接Id。 + */ + @Column(value = "dblink_id") + private Long dblinkId; + + /** + * 字典表名称。 + */ + @Column(value = "table_name") + private String tableName; + + /** + * 全局字典编码。 + */ + @Column(value = "dict_code") + private String dictCode; + + /** + * 字典表键字段名称。 + */ + @Column(value = "key_column_name") + private String keyColumnName; + + /** + * 字典表父键字段名称。 + */ + @Column(value = "parent_key_column_name") + private String parentKeyColumnName; + + /** + * 字典值字段名称。 + */ + @Column(value = "value_column_name") + private String valueColumnName; + + /** + * 逻辑删除字段。 + */ + @Column(value = "deleted_column_name") + private String deletedColumnName; + + /** + * 用户过滤滤字段名称。 + */ + @Column(value = "user_filter_column_name") + private String userFilterColumnName; + + /** + * 部门过滤字段名称。 + */ + @Column(value = "dept_filter_column_name") + private String deptFilterColumnName; + + /** + * 租户过滤字段名称。 + */ + @Column(value = "tenant_filter_column_name") + private String tenantFilterColumnName; + + /** + * 是否树形标记。 + */ + @Column(value = "tree_flag") + private Boolean treeFlag; + + /** + * 获取字典数据的url。 + */ + @Column(value = "dict_list_url") + private String dictListUrl; + + /** + * 根据主键id批量获取字典数据的url。 + */ + @Column(value = "dict_ids_url") + private String dictIdsUrl; + + /** + * 字典的JSON数据。 + */ + @Column(value = "dict_data_json") + private String dictDataJson; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + @RelationConstDict( + masterIdField = "dictType", + constantDictClass = DictType.class) + @Column(ignore = true) + private Map dictTypeDictMap; + + @RelationDict( + masterIdField = "dblinkId", + slaveModelClass = OnlineDblink.class, + slaveIdField = "dblinkId", + slaveNameField = "dblinkName") + @Column(ignore = true) + private Map dblinkIdDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java new file mode 100644 index 00000000..7b671060 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java @@ -0,0 +1,134 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.online.model.constant.FormType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_form") +public class OnlineForm { + + /** + * 主键Id。 + */ + @Id(value = "form_id") + private Long formId; + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 页面id。 + */ + @Column(value = "page_id") + private Long pageId; + + /** + * 表单编码。 + */ + @Column(value = "form_code") + private String formCode; + + /** + * 表单名称。 + */ + @Column(value = "form_name") + private String formName; + + /** + * 表单类别。 + */ + @Column(value = "form_kind") + private Integer formKind; + + /** + * 表单类型。 + */ + @Column(value = "form_type") + private Integer formType; + + /** + * 表单主表id。 + */ + @Column(value = "master_table_id") + private Long masterTableId; + + /** + * 表单组件JSON。 + */ + @Column(value = "widget_json") + private String widgetJson; + + /** + * 表单参数JSON。 + */ + @Column(value = "params_json") + private String paramsJson; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + @RelationOneToOne( + masterIdField = "masterTableId", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId") + @Column(ignore = true) + private OnlineTable onlineTable; + + @RelationDict( + masterIdField = "masterTableId", + equalOneToOneRelationField = "onlineTable", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId", + slaveNameField = "modelName") + @Column(ignore = true) + private Map masterTableIdDictMap; + + @RelationConstDict( + masterIdField = "formType", + constantDictClass = FormType.class) + @Column(ignore = true) + private Map formTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java new file mode 100644 index 00000000..4e756604 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 在线表单和数据源多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_form_datasource") +public class OnlineFormDatasource { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 表单Id。 + */ + @Column(value = "form_id") + private Long formId; + + /** + * 数据源Id。 + */ + @Column(value = "datasource_id") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java new file mode 100644 index 00000000..a4138a60 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java @@ -0,0 +1,105 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.model.constant.PageType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单所在页面实体对象。这里我们可以把页面理解为表单的容器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_page") +public class OnlinePage { + + /** + * 主键Id。 + */ + @Id(value = "page_id") + private Long pageId; + + /** + * 租户Id。 + */ + @Column(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 页面编码。 + */ + @Column(value = "page_code") + private String pageCode; + + /** + * 页面名称。 + */ + @Column(value = "page_name") + private String pageName; + + /** + * 页面类型。 + */ + @Column(value = "page_type") + private Integer pageType; + + /** + * 页面编辑状态。 + */ + @Column(value = "status") + private Integer status; + + /** + * 是否发布。 + */ + @Column(value = "published") + private Boolean published; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + @RelationConstDict( + masterIdField = "pageType", + constantDictClass = PageType.class) + @Column(ignore = true) + private Map pageTypeDictMap; + + @RelationConstDict( + masterIdField = "status", + constantDictClass = PageStatus.class) + @Column(ignore = true) + private Map statusDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java new file mode 100644 index 00000000..7326f684 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 在线表单页面和数据源多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_page_datasource") +public class OnlinePageDatasource { + + /** + * 主键Id。 + */ + @Id(value = "id") + private Long id; + + /** + * 页面主键Id。 + */ + @Column(value = "page_id") + private Long pageId; + + /** + * 数据源主键Id。 + */ + @Column(value = "datasource_id") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java new file mode 100644 index 00000000..174250ac --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java @@ -0,0 +1,98 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.online.model.constant.RuleType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段验证规则实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_rule") +public class OnlineRule { + + /** + * 主键Id。 + */ + @Id(value = "rule_id") + private Long ruleId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 规则名称。 + */ + @Column(value = "rule_name") + private String ruleName; + + /** + * 规则类型。 + */ + @Column(value = "rule_type") + private Integer ruleType; + + /** + * 内置规则标记。 + */ + @Column(value = "builtin") + private Boolean builtin; + + /** + * 自定义规则的正则表达式。 + */ + @Column(value = "pattern") + private String pattern; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @Column(value = "deleted_flag", isLogicDelete = true) + private Integer deletedFlag; + + /** + * ruleId 的多对多关联表数据对象。 + */ + @Column(ignore = true) + private OnlineColumnRule onlineColumnRule; + + @RelationConstDict( + masterIdField = "ruleType", + constantDictClass = RuleType.class) + @Column(ignore = true) + private Map ruleTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java new file mode 100644 index 00000000..c98bb765 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java @@ -0,0 +1,99 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import com.orangeforms.common.core.annotation.RelationOneToMany; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单的数据表实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_table") +public class OnlineTable { + + /** + * 主键Id。 + */ + @Id(value = "table_id") + private Long tableId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Column(value = "app_code") + private String appCode; + + /** + * 表名称。 + */ + @Column(value = "table_name") + private String tableName; + + /** + * 实体名称。 + */ + @Column(value = "model_name") + private String modelName; + + /** + * 数据库链接Id。 + */ + @Column(value = "dblink_id") + private Long dblinkId; + + /** + * 创建时间。 + */ + @Column(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @Column(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @Column(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @Column(value = "update_user_id") + private Long updateUserId; + + @RelationOneToMany( + masterIdField = "tableId", + slaveModelClass = OnlineColumn.class, + slaveIdField = "tableId") + @Column(ignore = true) + private List columnList; + + /** + * 该字段会被缓存,因此在线表单执行操作时可以从缓存中读取该数据,并可基于columnId进行快速检索。 + */ + @Column(ignore = true) + private Map columnMap; + + /** + * 当前表的主键字段,该字段仅仅用于动态表单运行时的SQL拼装。 + */ + @Column(ignore = true) + private OnlineColumn primaryKeyColumn; + + /** + * 当前表的逻辑删除字段,该字段仅仅用于动态表单运行时的SQL拼装。 + */ + @Column(ignore = true) + private OnlineColumn logicDeleteColumn; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java new file mode 100644 index 00000000..70aee6ff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.online.model; + +import com.mybatisflex.annotation.*; +import lombok.Data; + +/** + * 在线数据表虚拟字段实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Table(value = "zz_online_virtual_column") +public class OnlineVirtualColumn { + + /** + * 主键Id。 + */ + @Id(value = "virtual_column_id") + private Long virtualColumnId; + + /** + * 所在表Id。 + */ + @Column(value = "table_id") + private Long tableId; + + /** + * 字段名称。 + */ + @Column(value = "object_field_name") + private String objectFieldName; + + /** + * 属性类型。 + */ + @Column(value = "object_field_type") + private String objectFieldType; + + /** + * 字段提示名。 + */ + @Column(value = "column_prompt") + private String columnPrompt; + + /** + * 虚拟字段类型(0: 聚合)。 + */ + @Column(value = "virtual_type") + private Integer virtualType; + + /** + * 关联数据源Id。 + */ + @Column(value = "datasource_id") + private Long datasourceId; + + /** + * 关联Id。 + */ + @Column(value = "relation_id") + private Long relationId; + + /** + * 聚合字段所在关联表Id。 + */ + @Column(value = "aggregation_table_id") + private Long aggregationTableId; + + /** + * 关联表聚合字段Id。 + */ + @Column(value = "aggregation_column_id") + private Long aggregationColumnId; + + /** + * 聚合类型(0: count 1: sum 2: avg 3: max 4:min)。 + */ + @Column(value = "aggregation_type") + private Integer aggregationType; + + /** + * 存储过滤条件的json。 + */ + @Column(value = "where_clause_json") + private String whereClauseJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java new file mode 100644 index 00000000..6287a355 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java @@ -0,0 +1,79 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FieldFilterType { + + /** + * 无过滤。 + */ + public static final int NO_FILTER = 0; + /** + * 等于过滤。 + */ + public static final int EQUAL_FILTER = 1; + /** + * 范围过滤。 + */ + public static final int RANGE_FILTER = 2; + /** + * 模糊过滤。 + */ + public static final int LIKE_FILTER = 3; + /** + * IN LIST列表过滤。 + */ + public static final int IN_LIST_FILTER = 4; + /** + * 用OR连接的多个模糊查询。 + */ + public static final int MULTI_LIKE = 5; + /** + * NOT IN LIST列表过滤。 + */ + public static final int NOT_IN_LIST_FILTER = 6; + /** + * NOT IN LIST列表过滤。 + */ + public static final int IS_NULL = 7; + /** + * NOT IN LIST列表过滤。 + */ + public static final int IS_NOT_NULL = 8; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(NO_FILTER, "无过滤"); + DICT_MAP.put(EQUAL_FILTER, "等于过滤"); + DICT_MAP.put(RANGE_FILTER, "范围过滤"); + DICT_MAP.put(LIKE_FILTER, "模糊过滤"); + DICT_MAP.put(IN_LIST_FILTER, "IN LIST列表过滤"); + DICT_MAP.put(MULTI_LIKE, "用OR连接的多个模糊查询"); + DICT_MAP.put(NOT_IN_LIST_FILTER, "NOT IN LIST列表过滤"); + DICT_MAP.put(IS_NULL, "IS NULL"); + DICT_MAP.put(IS_NOT_NULL, "IS NOT NULL"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldFilterType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java new file mode 100644 index 00000000..d8afef0b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java @@ -0,0 +1,109 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段类别常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FieldKind { + + /** + * 文件上传字段。 + */ + public static final int UPLOAD = 1; + /** + * 图片上传字段。 + */ + public static final int UPLOAD_IMAGE = 2; + /** + * 富文本字段。 + */ + public static final int RICH_TEXT = 3; + /** + * 字典多选字段。 + */ + public static final int DICT_MULTI_SELECT = 4; + /** + * 创建人部门Id。 + */ + public static final int CREATE_DEPT_ID = 19; + /** + * 创建时间字段。 + */ + public static final int CREATE_TIME = 20; + /** + * 创建人字段。 + */ + public static final int CREATE_USER_ID = 21; + /** + * 更新时间字段。 + */ + public static final int UPDATE_TIME = 22; + /** + * 更新人字段。 + */ + public static final int UPDATE_USER_ID = 23; + /** + * 包含自动编码。 + */ + public static final int AUTO_CODE = 24; + /** + * 流程最后审批状态。 + */ + public static final int FLOW_APPROVAL_STATUS = 25; + /** + * 流程结束状态。 + */ + public static final int FLOW_FINISHED_STATUS = 26; + /** + * 脱敏字段。 + */ + public static final int MASK_FIELD = 27; + /** + * 租户过滤字段。 + */ + public static final int TENANT_FILTER = 28; + /** + * 逻辑删除字段。 + */ + public static final int LOGIC_DELETE = 31; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(UPLOAD, "文件上传字段"); + DICT_MAP.put(UPLOAD_IMAGE, "图片上传字段"); + DICT_MAP.put(RICH_TEXT, "富文本字段"); + DICT_MAP.put(DICT_MULTI_SELECT, "字典多选字段"); + DICT_MAP.put(CREATE_DEPT_ID, "创建人部门字段"); + DICT_MAP.put(CREATE_TIME, "创建时间字段"); + DICT_MAP.put(CREATE_USER_ID, "创建人字段"); + DICT_MAP.put(UPDATE_TIME, "更新时间字段"); + DICT_MAP.put(UPDATE_USER_ID, "更新人字段"); + DICT_MAP.put(AUTO_CODE, "自动编码字段"); + DICT_MAP.put(FLOW_APPROVAL_STATUS, "流程最后审批状态"); + DICT_MAP.put(FLOW_FINISHED_STATUS, "流程结束状态"); + DICT_MAP.put(MASK_FIELD, "脱敏字段"); + DICT_MAP.put(TENANT_FILTER, "租户过滤字段"); + DICT_MAP.put(LOGIC_DELETE, "逻辑删除字段"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldKind() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java new file mode 100644 index 00000000..71b22651 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表单类别常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FormKind { + + /** + * 弹框。 + */ + public static final int DIALOG = 1; + /** + * 跳页。 + */ + public static final int NEW_PAGE = 5; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(DIALOG, "弹框列表"); + DICT_MAP.put(NEW_PAGE, "跳页类别"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FormKind() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java new file mode 100644 index 00000000..6b969c20 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java @@ -0,0 +1,64 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表单类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FormType { + + /** + * 查询表单。 + */ + public static final int QUERY = 1; + /** + * 左树右表表单。 + */ + public static final int ADVANCED_QUERY = 2; + /** + * 一对一关联数据查询。 + */ + public static final int ONE_TO_ONE_QUERY = 3; + /** + * 编辑表单。 + */ + public static final int EDIT_FORM = 5; + /** + * 流程表单。 + */ + public static final int FLOW = 10; + /** + * 流程工单表单。 + */ + public static final int FLOW_WORK_ORDER = 11; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(QUERY, "查询表单"); + DICT_MAP.put(ADVANCED_QUERY, "左树右表表单"); + DICT_MAP.put(ONE_TO_ONE_QUERY, "一对一关联数据查询"); + DICT_MAP.put(EDIT_FORM, "编辑表单"); + DICT_MAP.put(FLOW, "流程表单"); + DICT_MAP.put(FLOW_WORK_ORDER, "流程工单表单"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FormType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java new file mode 100644 index 00000000..6eed451d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 页面状态常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class PageStatus { + + /** + * 编辑基础信息。 + */ + public static final int BASIC = 0; + /** + * 编辑数据模型。 + */ + public static final int DATASOURCE = 1; + /** + * 设计表单。 + */ + public static final int FORM_DESIGN = 2; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(BASIC, "编辑基础信息"); + DICT_MAP.put(DATASOURCE, "编辑数据模型"); + DICT_MAP.put(FORM_DESIGN, "设计表单"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private PageStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java new file mode 100644 index 00000000..45e614a5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 页面类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class PageType { + + /** + * 业务页面。 + */ + public static final int BIZ = 1; + /** + * 统计页面。 + */ + public static final int STATS = 5; + /** + * 流程页面。 + */ + public static final int FLOW = 10; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(BIZ, "业务页面"); + DICT_MAP.put(STATS, "统计页面"); + DICT_MAP.put(FLOW, "流程页面"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private PageType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java new file mode 100644 index 00000000..f14289da --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 关联类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class RelationType { + + /** + * 一对一关联。 + */ + public static final int ONE_TO_ONE = 0; + /** + * 一对多关联。 + */ + public static final int ONE_TO_MANY = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(ONE_TO_ONE, "一对一关联"); + DICT_MAP.put(ONE_TO_MANY, "一对多关联"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RelationType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java new file mode 100644 index 00000000..f2b5ee76 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 验证规则类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class RuleType { + + /** + * 只允许整数。 + */ + public static final int INTEGER_ONLY = 1; + /** + * 只允许数字。 + */ + public static final int DIGITAL_ONLY = 2; + /** + * 只允许英文字符。 + */ + public static final int LETTER_ONLY = 3; + /** + * 范围验证。 + */ + public static final int RANGE = 4; + /** + * 邮箱格式验证。 + */ + public static final int EMAIL = 5; + /** + * 手机格式验证。 + */ + public static final int MOBILE = 6; + /** + * 自定义验证。 + */ + public static final int CUSTOM = 100; + + private static final Map DICT_MAP = new HashMap<>(7); + static { + DICT_MAP.put(INTEGER_ONLY, "只允许整数"); + DICT_MAP.put(DIGITAL_ONLY, "只允许数字"); + DICT_MAP.put(LETTER_ONLY, "只允许英文字符"); + DICT_MAP.put(RANGE, "范围验证"); + DICT_MAP.put(EMAIL, "邮箱格式验证"); + DICT_MAP.put(MOBILE, "手机格式验证"); + DICT_MAP.put(CUSTOM, "自定义验证"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RuleType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java new file mode 100644 index 00000000..3d5b9c42 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 在线表单虚拟字段类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class VirtualType { + + /** + * 聚合。 + */ + public static final int AGGREGATION = 0; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(AGGREGATION, "聚合"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private VirtualType() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java new file mode 100644 index 00000000..8b6291f6 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.online.object; + +import com.orangeforms.common.online.model.OnlineColumn; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 表字段数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ColumnData { + + /** + * 在线表字段对象。 + */ + private OnlineColumn column; + + /** + * 字段值。 + */ + private Object columnValue; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java new file mode 100644 index 00000000..f99e18d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.online.object; + +import lombok.Data; + +import java.util.List; + +/** + * 在线表单常量字典的数据结构。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class ConstDictInfo { + + private List dictData; + + @Data + public static class ConstDictData { + private String type; + private Object id; + private String name; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java new file mode 100644 index 00000000..4798b332 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.online.object; + +import lombok.Data; + +/** + * 连接表信息对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class JoinTableInfo { + + /** + * 是否左连接。 + */ + private Boolean leftJoin; + + /** + * 连接表表名。 + */ + private String joinTableName; + + /** + * 连接条件。 + */ + private String joinCondition; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java new file mode 100644 index 00000000..a48a487e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java @@ -0,0 +1,147 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineColumnRule; + +import java.util.List; +import java.util.Set; + +/** + * 字段数据数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineColumnService extends IBaseService { + + /** + * 保存新增数据表字段列表。 + * + * @param columnList 新增数据表字段对象列表。 + * @param onlineTableId 在线表对象的主键Id。 + * @return 插入的在线表字段数据。 + */ + List saveNewList(List columnList, Long onlineTableId); + + /** + * 更新数据对象。 + * + * @param onlineColumn 更新的对象。 + * @param originalOnlineColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineColumn onlineColumn, OnlineColumn originalOnlineColumn); + + /** + * 刷新数据库表字段的数据到在线表字段。 + * + * @param sqlTableColumn 源数据库表字段对象。 + * @param onlineColumn 被刷新的在线表字段对象。 + */ + void refresh(SqlTableColumn sqlTableColumn, OnlineColumn onlineColumn); + + /** + * 删除指定数据。 + * + * @param tableId 表Id。 + * @param columnId 字段Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long tableId, Long columnId); + + /** + * 批量添加多对多关联关系。 + * + * @param onlineColumnRuleList 多对多关联表对象集合。 + * @param columnId 主表Id。 + */ + void addOnlineColumnRuleList(List onlineColumnRuleList, Long columnId); + + /** + * 更新中间表数据。 + * + * @param onlineColumnRule 中间表对象。 + * @return 更新成功与否。 + */ + boolean updateOnlineColumnRule(OnlineColumnRule onlineColumnRule); + + /** + * 获取中间表数据。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 中间表对象。 + */ + OnlineColumnRule getOnlineColumnRule(Long columnId, Long ruleId); + + /** + * 移除单条多对多关系。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 成功返回true,否则false。 + */ + boolean removeOnlineColumnRule(Long columnId, Long ruleId); + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param tableId 主表主键Id。 + * @return 删除数量。 + */ + int removeByTableId(Long tableId); + + /** + * 删除指定数据表Id集合中的表字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + void removeByTableIdSet(Set tableIdSet); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @return 查询结果集。 + */ + List getOnlineColumnList(OnlineColumn filter); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @return 查询结果集。 + */ + List getOnlineColumnListWithRelation(OnlineColumn filter); + + /** + * 获取指定数据表Id集合的字段对象列表。 + * + * @param tableIdSet 指定的数据表Id集合。 + * @return 数据表Id集合所包含的字段对象列表。 + */ + List getOnlineColumnListByTableIds(Set tableIdSet); + + /** + * 根据表Id和字段列名获取指定字段。 + * + * @param tableId 字段所在表Id。 + * @param columnName 字段名。 + * @return 查询出的字段对象。 + */ + OnlineColumn getOnlineColumnByTableIdAndColumnName(Long tableId, String columnName); + + /** + * 验证主键是否正确。 + * + * @param tableColumn 数据库导入的表字段对象。 + * @return 验证结果。 + */ + CallResult verifyPrimaryKey(SqlTableColumn tableColumn); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java new file mode 100644 index 00000000..a96d86b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java @@ -0,0 +1,85 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; + +import java.util.List; +import java.util.Set; + +/** + * 数据关联数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceRelationService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param relation 新增对象。 + * @param slaveSqlTable 新增的关联从数据表对象。 + * @param slaveSqlColumn 新增的关联从数据表对象。 + * @return 返回新增对象。 + */ + OnlineDatasourceRelation saveNew( + OnlineDatasourceRelation relation, SqlTable slaveSqlTable, SqlTableColumn slaveSqlColumn); + + /** + * 更新数据对象。 + * + * @param relation 更新的对象。 + * @param originalRelation 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDatasourceRelation relation, OnlineDatasourceRelation originalRelation); + + /** + * 删除指定数据。 + * + * @param relationId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long relationId); + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param datasourceId 主表主键Id。 + * @return 删除数量。 + */ + int removeByDatasourceId(Long datasourceId); + + /** + * 查询指定数据源Id的数据源关联对象列表。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 在线数据源关联对象列表。 + */ + List getOnlineDatasourceRelationListFromCache(Set datasourceIdSet); + + /** + * 查询指定数据源关联对象。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @return 在线数据源关联对象。 + */ + OnlineDatasourceRelation getOnlineDatasourceRelationFromCache(Long datasourceId, Long relationId); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceRelationList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceRelationListWithRelation( + OnlineDatasourceRelation filter, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java new file mode 100644 index 00000000..f51dddb5 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java @@ -0,0 +1,134 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceTable; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 数据模型数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineDatasource 新增对象。 + * @param sqlTable 新增的数据表对象。 + * @param pageId 关联的页面Id。 + * @return 返回新增对象。 + */ + OnlineDatasource saveNew(OnlineDatasource onlineDatasource, SqlTable sqlTable, Long pageId); + + /** + * 更新数据对象。 + * + * @param onlineDatasource 更新的对象。 + * @param originalOnlineDatasource 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDatasource onlineDatasource, OnlineDatasource originalOnlineDatasource); + + /** + * 删除指定数据。 + * + * @param datasourceId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long datasourceId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDatasourceListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceList(OnlineDatasource filter, String orderBy); + + /** + * 查询指定数据源Id的数据源对象。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceId 数据源Id。 + * @return 在线数据源对象。 + */ + OnlineDatasource getOnlineDatasourceFromCache(Long datasourceId); + + /** + * 查询指定数据源Id集合的数据源列表。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 在线数据源对象集合。 + */ + List getOnlineDatasourceListFromCache(Set datasourceIdSet); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceListWithRelation(OnlineDatasource filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param pageId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceListByPageId(Long pageId, OnlineDatasource filter, String orderBy); + + /** + * 获取指定数据源Id集合所关联的在线表关联数据。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 数据源和数据表的多对多关联列表。 + */ + List getOnlineDatasourceTableList(Set datasourceIdSet); + + /** + * 根据在线表单Id集合,获取关联的在线数据源对象列表。 + * + * @param readFormIdSet 在线表单Id集合。 + * @return 与参数表单Id关联的数据源列表。 + */ + List getOnlineDatasourceListByFormIds(Set readFormIdSet); + + /** + * 根据主表Id获取在线表单数据源对象。 + * + * @param masterTableId 主表Id。 + * @return 在线表单数据源对象。 + */ + OnlineDatasource getOnlineDatasourceByMasterTableId(Long masterTableId); + + /** + * 判断指定数据源变量是否存在。 + * @param variableName 变量名。 + * @return true存在,否则false。 + */ + boolean existByVariableName(String variableName); + + /** + * 获取在线表单页面和在线表单数据源变量名的映射关系。 + * + * @param pageIds 页面Id集合。 + * @return 在线表单页面和在线表单数据源变量名的映射关系。 + */ + Map getPageIdAndVariableNameMapByPageIds(Set pageIds); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java new file mode 100644 index 00000000..d04ace46 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java @@ -0,0 +1,99 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.online.model.OnlineDblink; + +import java.util.List; + +/** + * 数据库链接数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDblinkService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineDblink 新增对象。 + * @return 返回新增对象。 + */ + OnlineDblink saveNew(OnlineDblink onlineDblink); + + /** + * 更新数据对象。 + * + * @param onlineDblink 更新的对象。 + * @param originalOnlineDblink 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDblink onlineDblink, OnlineDblink originalOnlineDblink); + + /** + * 删除指定数据。 + * + * @param dblinkId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long dblinkId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDblinkListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDblinkList(OnlineDblink filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDblinkList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDblinkListWithRelation(OnlineDblink filter, String orderBy); + + /** + * 获取指定DBLink下面的全部数据表。 + * + * @param dblink 数据库链接对象。 + * @return 全部数据表列表。 + */ + List getDblinkTableList(OnlineDblink dblink); + + /** + * 获取指定DBLink下,指定表名的数据表对象,及其关联字段列表。 + * + * @param dblink 数据库链接对象。 + * @param tableName 数据库中的数据表名。 + * @return 数据表对象。 + */ + SqlTable getDblinkTable(OnlineDblink dblink, String tableName); + + /** + * 获取指定DBLink下,指定表名的字段列表。 + * + * @param dblink 数据库链接对象。 + * @param tableName 数据库中的数据表名。 + * @return 表的字段列表。 + */ + List getDblinkTableColumnList(OnlineDblink dblink, String tableName); + + /** + * 获取指定DBLink下,指定表的字段对象。 + * + * @param dblink 数据库链接对象。 + * @param tableName 数据库中的数据表名。 + * @param columnName 数据库中的数据表的字段名。 + * @return 表的字段对象。 + */ + SqlTableColumn getDblinkTableColumn(OnlineDblink dblink, String tableName, String columnName); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java new file mode 100644 index 00000000..4f2c56bd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java @@ -0,0 +1,78 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineDict; + +import java.util.List; +import java.util.Set; + +/** + * 在线表单字典数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDictService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineDict 新增对象。 + * @return 返回新增对象。 + */ + OnlineDict saveNew(OnlineDict onlineDict); + + /** + * 更新数据对象。 + * + * @param onlineDict 更新的对象。 + * @param originalOnlineDict 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDict onlineDict, OnlineDict originalOnlineDict); + + /** + * 删除指定数据。 + * + * @param dictId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long dictId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDictListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDictList(OnlineDict filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDictList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDictListWithRelation(OnlineDict filter, String orderBy); + + /** + * 从缓存中获取字典数据。 + * + * @param dictId 字典Id。 + * @return 在线字典对象。 + */ + OnlineDict getOnlineDictFromCache(Long dictId); + + /** + * 从缓存中获取字典数据集合。 + * + * @param dictIdSet 字典Id集合。 + * @return 在线字典对象集合。 + */ + List getOnlineDictListFromCache(Set dictIdSet); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java new file mode 100644 index 00000000..b6334b8d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java @@ -0,0 +1,122 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineForm; +import com.orangeforms.common.online.model.OnlineFormDatasource; + +import java.util.List; +import java.util.Set; + +/** + * 在线表单数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineFormService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineForm 新增对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 返回新增对象。 + */ + OnlineForm saveNew(OnlineForm onlineForm, Set datasourceIdSet); + + /** + * 更新数据对象。 + * + * @param onlineForm 更新的对象。 + * @param originalOnlineForm 原有数据对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineForm onlineForm, OnlineForm originalOnlineForm, Set datasourceIdSet); + + /** + * 删除指定数据。 + * + * @param formId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long formId); + + /** + * 根据PageId,删除其所属的所有表单,以及表单关联的数据源数据。 + * + * @param pageId 指定的pageId。 + * @return 删除数量。 + */ + int removeByPageId(Long pageId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineFormListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineFormList(OnlineForm filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineFormList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineFormListWithRelation(OnlineForm filter, String orderBy); + + /** + * 获取使用指定数据表的表单列表。 + * + * @param tableId 数据表Id。 + * @return 使用该数据表的表单列表。 + */ + List getOnlineFormListByTableId(Long tableId); + + /** + * 获取指定表单的数据源列表。 + * 从缓存中读取,如果缓存中不存在,从数据库读取后同步更新到缓存。 + * + * @param formId 指定的表单。 + * @return 表单和数据源的多对多关联对象列表。 + */ + List getFormDatasourceListFromCache(Long formId); + + /** + * 查询正在使用当前数据源的表单。 + * + * @param datasourceId 数据源Id。 + * @return 正在使用当前数据源的表单列表。 + */ + List getOnlineFormListByDatasourceId(Long datasourceId); + + /** + * 查询指定PageId集合的在线表单列表。 + * + * @param pageIdSet 页面Id集合。 + * @return 在线表单集合。 + */ + List getOnlineFormListByPageIds(Set pageIdSet); + + /** + * 从缓存中获取表单数据。 + * + * @param formId 表单Id。 + * @return 在线表单对象。 + */ + OnlineForm getOnlineFormFromCache(Long formId); + + /** + * 判断指定编码的表单是否存在。 + * + * @param formCode 表单编码。 + * @return true存在,否则false。 + */ + boolean existByFormCode(String formCode); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java new file mode 100644 index 00000000..9cde49b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java @@ -0,0 +1,220 @@ +package com.orangeforms.common.online.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineDict; +import com.orangeforms.common.online.model.OnlineTable; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 在线表单运行时操作的数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineOperationService { + + /** + * 待批量插入的所有表数据。 + * + * @param table 在线表对象。 + * @param dataList 数据对象列表。 + */ + void saveNewBatch(OnlineTable table, List dataList); + + /** + * 待插入的所有表数据。 + * + * @param table 在线表对象。 + * @param data 数据对象。 + * @return 主键值。由于自增主键不能获取插入后的主键值,因此返回NULL。 + */ + Object saveNew(OnlineTable table, JSONObject data); + + /** + * 待插入的主表数据和多个从表数据。 + * + * @param masterTable 主表在线表对象。 + * @param masterData 主表数据对象。 + * @param slaveDataListMap 多个从表的数据字段数据。 + * @return 主表的主键值。由于自增主键不能获取插入后的主键值,因此返回NULL。 + */ + Object saveNewWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap); + + /** + * 更新表数据。 + * + * @param table 在线表对象。 + * @param data 单条表数据。 + * @return true 更新成功,否则false。 + */ + boolean update(OnlineTable table, JSONObject data); + + /** + * 更新流程字段的状态。 + * + * @param table 数据表。 + * @param dataId 主键Id。 + * @param column 更新字段。 + * @param dataValue 新的数据值。 + * @return true 更新成功,否则false。 + */ + boolean updateColumn(OnlineTable table, String dataId, OnlineColumn column, T dataValue); + + /** + * 级联更新主表和从表数据。 + * + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param datasourceId 主表数据源Id。 + * @param slaveDataListMap 关联从表数据。 + */ + void updateWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Long datasourceId, + Map> slaveDataListMap); + + /** + * 更新关联从表的数据。 + * + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param masterDataId 主表主键Id。 + * @param datasourceId 主表数据源Id。 + * @param relationId 关联Id。 + * @param slaveDataList 从表数据。 + */ + void updateRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + Long datasourceId, + Long relationId, + List slaveDataList); + + /** + * 删除主表数据,及其需要级联删除的一对多关联从表数据。 + * + * @param table 表对象。 + * @param relationList 一对多关联对象列表。 + * @param dataId 主表主键Id值。 + * @return true 删除成功,否则false。 + */ + boolean delete(OnlineTable table, List relationList, String dataId); + + /** + * 删除一对多从表数据中的关联数据。 + * 删除所有字段为slaveColumn,数据值为columnValue,但是主键值不在keptIdSet中的从表关联数据。 + * + * @param slaveTable 一对多从表。 + * @param slaveColumn 从表关联字段。 + * @param columnValue 关联字段的值。 + * @param keptIdSet 被保留从表数据的主键Id值。 + */ + void deleteOneToManySlaveData( + OnlineTable slaveTable, OnlineColumn slaveColumn, String columnValue, Set keptIdSet); + + /** + * 根据主键判断当前数据是否存在。 + * + * @param table 主表对象。 + * @param dataId 主表主键Id值。 + * @return 存在返回true,否则false。 + */ + boolean existId(OnlineTable table, String dataId); + + /** + * 从数据源和一对一数据源关联中,动态获取数据。 + * + * @param table 主表对象。 + * @param oneToOneRelationList 数据源一对一关联列表。 + * @param allRelationList 数据源全部关联列表。 + * @param dataId 主表主键Id值。 + * @return 查询结果。 + */ + Map getMasterData( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + String dataId); + + /** + * 从一对多数据源关联中,动态获取数据。 + * + * @param relation 一对多数据源关联对象。 + * @param dataId 一对多关联数据主键Id值。 + * @return 查询结果。 + */ + Map getSlaveData(OnlineDatasourceRelation relation, String dataId); + + /** + * 从数据源和一对一数据源关联中,动态获取数据列表。 + * + * @param table 主表对象。 + * @param oneToOneRelationList 数据源一对一关联列表。 + * @param allRelationList 数据源全部关联列表。 + * @param filterList 过滤参数列表。 + * @param orderBy 排序字符串。 + * @param pageParam 分页对象。 + * @return 查询结果集。 + */ + MyPageData> getMasterDataList( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + List filterList, + String orderBy, + MyPageParam pageParam); + + /** + * 从一对多数据源关联中,动态获取数据列表。 + * + * @param relation 一对多数据源关联对象。 + * @param filterList 过滤参数列表。 + * @param orderBy 排序字符串。 + * @param pageParam 分页对象。 + * @return 查询结果集。 + */ + MyPageData> getSlaveDataList( + OnlineDatasourceRelation relation, List filterList, String orderBy, MyPageParam pageParam); + + /** + * 从字典对象指向的数据表中查询数据,并根据参数进行数据过滤。 + * + * @param dict 字典对象。 + * @param filterList 过滤参数列表。 + * @return 查询结果集。 + */ + List> getDictDataList(OnlineDict dict, List filterList); + + /** + * 为主表及其关联表数据绑定字典数据。 + * + * @param masterTable 主表对象。 + * @param relationList 主表依赖的关联列表。 + * @param dataList 数据列表。 + */ + void buildDataListWithDict( + OnlineTable masterTable, List relationList, List> dataList); + + /** + * 获取在线表单所关联的权限数据,包括权限字列表和权限资源列表。 + * + * @param menuFormIds 菜单关联的表单Id集合。 + * @param viewFormIds 查询权限的表单Id集合。 + * @param editFormIds 编辑权限的表单Id集合。 + * @return 在线表单权限数据。 + */ + Map calculatePermData(Set menuFormIds, Set viewFormIds, Set editFormIds); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java new file mode 100644 index 00000000..2ba8458b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java @@ -0,0 +1,138 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlinePage; +import com.orangeforms.common.online.model.OnlinePageDatasource; + +import java.util.List; + +/** + * 在线表单页面数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlinePageService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlinePage 新增对象。 + * @return 返回新增对象。 + */ + OnlinePage saveNew(OnlinePage onlinePage); + + /** + * 更新数据对象。 + * + * @param onlinePage 更新的对象。 + * @param originalOnlinePage 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlinePage onlinePage, OnlinePage originalOnlinePage); + + /** + * 更新页面对象的发布状态。 + * + * @param pageId 页面对象Id。 + * @param published 新的状态。 + */ + void updatePublished(Long pageId, Boolean published); + + /** + * 删除指定数据,及其包含的表单和数据源等。 + * + * @param pageId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long pageId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlinePageListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlinePageList(OnlinePage filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlinePageList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlinePageListWithRelation(OnlinePage filter, String orderBy); + + /** + * 批量添加多对多关联关系。 + * + * @param onlinePageDatasourceList 多对多关联表对象集合。 + * @param pageId 主表Id。 + */ + void addOnlinePageDatasourceList(List onlinePageDatasourceList, Long pageId); + + /** + * 获取中间表数据。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 中间表对象。 + */ + OnlinePageDatasource getOnlinePageDatasource(Long pageId, Long datasourceId); + + /** + * 获取在线页面和数据源中间表数据列表。 + * + * @param pageId 主表Id。 + * @return 在线页面和数据源中间表对象列表。 + */ + List getOnlinePageDatasourceListByPageId(Long pageId); + + /** + * 根据数据源Id,返回使用该数据源的OnlinePage对象。 + * + * @param datasourceId 数据源Id。 + * @return 使用该数据源的页面列表。 + */ + List getOnlinePageListByDatasourceId(Long datasourceId); + + /** + * 移除单条多对多关系。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 成功返回true,否则false。 + */ + boolean removeOnlinePageDatasource(Long pageId, Long datasourceId); + + /** + * 判断指定编码的页面是否存在。 + * + * @param pageCode 页面编码。 + * @return true存在,否则false。 + */ + boolean existByPageCode(String pageCode); + + /** + * 查询主键Id集合中不存在的,且租户Id为NULL的在线表单页面列表。 + * + * @param pageIds 主键Id集合。 + * @param orderBy 排序字符串。 + * @return 在线表单页面列表。 + */ + List getNotInListWithNonTenant(List pageIds, String orderBy); + + /** + * 查询主键Id集合中存在的,且租户Id为NULL的在线表单页面列表。 + * + * @param pageIds 主键Id集合。 + * @param orderBy 排序字符串。 + * @return 在线表单页面列表。 + */ + List getInListWithNonTenant(List pageIds, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java new file mode 100644 index 00000000..f381a43d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java @@ -0,0 +1,91 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineColumnRule; +import com.orangeforms.common.online.model.OnlineRule; + +import java.util.List; +import java.util.Set; + +/** + * 验证规则数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineRuleService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineRule 新增对象。 + * @return 返回新增对象。 + */ + OnlineRule saveNew(OnlineRule onlineRule); + + /** + * 更新数据对象。 + * + * @param onlineRule 更新的对象。 + * @param originalOnlineRule 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineRule onlineRule, OnlineRule originalOnlineRule); + + /** + * 删除指定数据。 + * + * @param ruleId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long ruleId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineRuleListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineRuleList(OnlineRule filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineRuleList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineRuleListWithRelation(OnlineRule filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getNotInOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy); + + /** + * 返回指定字段Id列表关联的字段规则对象列表。 + * + * @param columnIdSet 指定的字段Id列表。 + * @return 关联的字段规则对象列表。 + */ + List getOnlineColumnRuleListByColumnIds(Set columnIdSet); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java new file mode 100644 index 00000000..e30f7fba --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineTable; + +import java.util.List; +import java.util.Set; + +/** + * 数据表数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineTableService extends IBaseService { + + /** + * 基于数据库表保存新增对象。 + * + * @param sqlTable 数据库表对象。 + * @return 返回新增对象。 + */ + OnlineTable saveNewFromSqlTable(SqlTable sqlTable); + + /** + * 删除指定表及其关联的字段数据。 + * + * @param tableId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long tableId); + + /** + * 删除指定数据表Id集合中的表,及其关联字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + void removeByTableIdSet(Set tableIdSet); + + /** + * 根据数据源Id,获取该数据源及其关联所引用的数据表列表。 + * + * @param datasourceId 指定的数据源Id。 + * @return 该数据源及其关联所引用的数据表列表。 + */ + List getOnlineTableListByDatasourceId(Long datasourceId); + + /** + * 从缓存中获取指定的表数据及其关联字段列表。优先从缓存中读取,如果不存在则从数据库中读取,并同步到缓存。 + * 该接口方法仅仅用户在线表单的动态数据操作接口,而非在线表单的配置接口。 + * + * @param tableId 表主键Id。 + * @return 查询后的在线表对象。 + */ + OnlineTable getOnlineTableFromCache(Long tableId); + + /** + * 从缓存中获取指定的表字段。优先从缓存中读取,如果不存在则从数据库中读取,并同步到缓存。 + * 该接口方法仅仅用户在线表单的动态数据操作接口,而非在线表单的配置接口。 + * + * @param tableId 表主键Id。 + * @param columnId 字段Id。 + * @return 查询后的在线表对象。 + */ + OnlineColumn getOnlineColumnFromCache(Long tableId, Long columnId); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java new file mode 100644 index 00000000..710c3a51 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineVirtualColumn; + +import java.util.*; + +/** + * 虚拟字段数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineVirtualColumnService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineVirtualColumn 新增对象。 + * @return 返回新增对象。 + */ + OnlineVirtualColumn saveNew(OnlineVirtualColumn onlineVirtualColumn); + + /** + * 更新数据对象。 + * + * @param onlineVirtualColumn 更新的对象。 + * @param originalOnlineVirtualColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineVirtualColumn onlineVirtualColumn, OnlineVirtualColumn originalOnlineVirtualColumn); + + /** + * 删除指定数据。 + * + * @param virtualColumnId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long virtualColumnId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineVirtualColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineVirtualColumnList(OnlineVirtualColumn filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineVirtualColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineVirtualColumnListWithRelation(OnlineVirtualColumn filter, String orderBy); + + /** + * 根据数据表的集合,查询关联的虚拟字段数据列表。 + * @param tableIdSet 在线数据表Id集合。 + * @return 关联的虚拟字段数据列表。 + */ + List getOnlineVirtualColumnListByTableIds(Set tableIdSet); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java new file mode 100644 index 00000000..52b64742 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java @@ -0,0 +1,357 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Assert; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.util.OnlineDataSourceUtil; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.orangeforms.common.online.dao.OnlineColumnMapper; +import com.orangeforms.common.online.dao.OnlineColumnRuleMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineColumnRule; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.github.pagehelper.Page; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * 字段数据数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineColumnService") +public class OnlineColumnServiceImpl extends BaseService implements OnlineColumnService { + + @Autowired + private OnlineColumnMapper onlineColumnMapper; + @Autowired + private OnlineColumnRuleMapper onlineColumnRuleMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineColumnMapper; + } + + /** + * 保存新增数据表字段列表。 + * + * @param columnList 新增数据表字段对象列表。 + * @param onlineTableId 在线表对象的主键Id。 + * @return 插入的在线表字段数据。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public List saveNewList(List columnList, Long onlineTableId) { + List onlineColumnList = new LinkedList<>(); + if (CollUtil.isEmpty(columnList)) { + return onlineColumnList; + } + this.evictTableCache(onlineTableId); + for (SqlTableColumn column : columnList) { + OnlineColumn onlineColumn = new OnlineColumn(); + BeanUtil.copyProperties(column, onlineColumn, false); + onlineColumn.setColumnId(idGenerator.nextLongId()); + onlineColumn.setTableId(onlineTableId); + this.setDefault(column, onlineColumn); + onlineColumnMapper.insert(onlineColumn); + onlineColumnList.add(onlineColumn); + } + return onlineColumnList; + } + + /** + * 更新数据对象。 + * + * @param onlineColumn 更新的对象。 + * @param originalOnlineColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineColumn onlineColumn, OnlineColumn originalOnlineColumn) { + this.evictTableCache(onlineColumn.getTableId()); + onlineColumn.setUpdateTime(new Date()); + onlineColumn.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + onlineColumn.setCreateTime(originalOnlineColumn.getCreateTime()); + onlineColumn.setCreateUserId(originalOnlineColumn.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return onlineColumnMapper.update(onlineColumn, false) == 1; + } + + /** + * 刷新数据库表字段的数据到在线表字段。 + * + * @param sqlTableColumn 源数据库表字段对象。 + * @param onlineColumn 被刷新的在线表字段对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void refresh(SqlTableColumn sqlTableColumn, OnlineColumn onlineColumn) { + this.evictTableCache(onlineColumn.getTableId()); + BeanUtil.copyProperties(sqlTableColumn, onlineColumn, false); + String objectFieldName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, onlineColumn.getColumnName()); + onlineColumn.setObjectFieldName(objectFieldName); + String objectFieldType = convertToJavaType(onlineColumn, sqlTableColumn.getDblinkType()); + onlineColumn.setObjectFieldType(objectFieldType); + onlineColumnMapper.update(onlineColumn); + } + + /** + * 删除指定数据。 + * + * @param tableId 表Id。 + * @param columnId 字段Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long tableId, Long columnId) { + this.evictTableCache(tableId); + return onlineColumnMapper.deleteById(columnId) == 1; + } + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param tableId 主表主键Id。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int removeByTableId(Long tableId) { + return onlineColumnMapper.deleteByQuery(new QueryWrapper().eq(OnlineColumn::getTableId, tableId)); + } + + /** + * 删除指定数据表Id集合中的表字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByTableIdSet(Set tableIdSet) { + onlineColumnMapper.deleteByQuery(new QueryWrapper().in(OnlineColumn::getTableId, tableIdSet)); + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @return 查询结果集。 + */ + @Override + public List getOnlineColumnList(OnlineColumn filter) { + return onlineColumnMapper.getOnlineColumnList(filter); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @return 查询结果集。 + */ + @Override + public List getOnlineColumnListWithRelation(OnlineColumn filter) { + List resultList = onlineColumnMapper.getOnlineColumnList(filter); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 获取指定数据表Id集合的字段对象列表。 + * + * @param tableIdSet 指定的数据表Id集合。 + * @return 数据表Id集合所包含的字段对象列表。 + */ + @Override + public List getOnlineColumnListByTableIds(Set tableIdSet) { + return onlineColumnMapper.selectListByQuery(new QueryWrapper().in(OnlineColumn::getTableId, tableIdSet)); + } + + /** + * 根据表Id和字段列名获取指定字段。 + * + * @param tableId 字段所在表Id。 + * @param columnName 字段名。 + * @return 查询出的字段对象。 + */ + @Override + public OnlineColumn getOnlineColumnByTableIdAndColumnName(Long tableId, String columnName) { + OnlineColumn filter = new OnlineColumn(); + filter.setTableId(tableId); + filter.setColumnName(columnName); + return onlineColumnMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + @Override + public CallResult verifyPrimaryKey(SqlTableColumn tableColumn) { + Assert.isTrue(tableColumn.getPrimaryKey()); + OnlineColumn onlineColumn = new OnlineColumn(); + BeanUtil.copyProperties(tableColumn, onlineColumn, false); + String javaType = this.convertToJavaType(onlineColumn, tableColumn.getDblinkType()); + if (ObjectFieldType.INTEGER.equals(javaType)) { + if (BooleanUtil.isFalse(onlineColumn.getAutoIncrement())) { + return CallResult.error("字段验证失败,整型主键必须是自增主键!"); + } + } else { + if (!StrUtil.equalsAny(javaType, ObjectFieldType.LONG, ObjectFieldType.STRING)) { + return CallResult.error("字段验证失败,不合法的主键类型 [" + tableColumn.getColumnType() + "]!"); + } + } + return CallResult.ok(); + } + + /** + * 批量添加多对多关联关系。 + * + * @param onlineColumnRuleList 多对多关联表对象集合。 + * @param columnId 主表Id。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void addOnlineColumnRuleList(List onlineColumnRuleList, Long columnId) { + this.evictTableCacheByColumnId(columnId); + for (OnlineColumnRule onlineColumnRule : onlineColumnRuleList) { + onlineColumnRule.setColumnId(columnId); + onlineColumnRuleMapper.insert(onlineColumnRule); + } + } + + /** + * 更新中间表数据。 + * + * @param onlineColumnRule 中间表对象。 + * @return 更新成功与否。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateOnlineColumnRule(OnlineColumnRule onlineColumnRule) { + this.evictTableCacheByColumnId(onlineColumnRule.getColumnId()); + OnlineColumnRule filter = new OnlineColumnRule(); + filter.setColumnId(onlineColumnRule.getColumnId()); + filter.setRuleId(onlineColumnRule.getRuleId()); + return onlineColumnRuleMapper.updateByQuery(onlineColumnRule, false, QueryWrapper.create(filter)) > 0; + } + + /** + * 获取中间表数据。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 中间表对象。 + */ + @Override + public OnlineColumnRule getOnlineColumnRule(Long columnId, Long ruleId) { + OnlineColumnRule filter = new OnlineColumnRule(); + filter.setColumnId(columnId); + filter.setRuleId(ruleId); + return onlineColumnRuleMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + /** + * 移除单条多对多关系。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeOnlineColumnRule(Long columnId, Long ruleId) { + this.evictTableCacheByColumnId(columnId); + OnlineColumnRule filter = new OnlineColumnRule(); + filter.setColumnId(columnId); + filter.setRuleId(ruleId); + return onlineColumnRuleMapper.deleteByQuery(QueryWrapper.create(filter)) > 0; + } + + private void setDefault(SqlTableColumn column, OnlineColumn onlineColumn) { + String objectFieldName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, onlineColumn.getColumnName()); + onlineColumn.setObjectFieldName(objectFieldName); + String objectFieldType = convertToJavaType(onlineColumn, column.getDblinkType()); + onlineColumn.setObjectFieldType(objectFieldType); + onlineColumn.setFilterType(FieldFilterType.NO_FILTER); + onlineColumn.setParentKey(false); + onlineColumn.setDeptFilter(false); + onlineColumn.setUserFilter(false); + if (onlineColumn.getAutoIncrement() == null) { + onlineColumn.setAutoIncrement(false); + } + onlineColumn.setUploadFileSystemType(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal()); + Date now = new Date(); + onlineColumn.setUpdateTime(now); + onlineColumn.setCreateTime(now); + onlineColumn.setCreateUserId(TokenData.takeFromRequest().getUserId()); + onlineColumn.setUpdateUserId(onlineColumn.getCreateUserId()); + } + + private void evictTableCache(Long tableId) { + String tableIdKey = OnlineRedisKeyUtil.makeOnlineTableKey(tableId); + redissonClient.getBucket(tableIdKey).delete(); + } + + private void evictTableCacheByColumnId(Long columnId) { + OnlineColumn column = this.getById(columnId); + if (column != null) { + this.evictTableCache(column.getTableId()); + } + } + + private String convertToJavaType(OnlineColumn column, int dblinkType) { + DataSourceProvider provider = dataSourceUtil.getProvider(dblinkType); + if (provider == null) { + throw new MyRuntimeException("Unsupported Data Type"); + } + return provider.convertColumnTypeToJavaType( + column.getColumnType(), column.getNumericPrecision(), column.getNumericScale()); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java new file mode 100644 index 00000000..49b46f30 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java @@ -0,0 +1,285 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.orangeforms.common.online.dao.OnlineDatasourceRelationMapper; +import com.orangeforms.common.online.dao.OnlineDatasourceTableMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineDatasourceTable; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.github.pagehelper.Page; +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.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 数据源关联数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDatasourceRelationService") +public class OnlineDatasourceRelationServiceImpl + extends BaseService implements OnlineDatasourceRelationService { + + @Autowired + private OnlineDatasourceRelationMapper onlineDatasourceRelationMapper; + @Autowired + private OnlineDatasourceTableMapper onlineDatasourceTableMapper; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + @Autowired + private CommonRedisUtil commonRedisUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDatasourceRelationMapper; + } + + /** + * 保存新增对象。 + * + * @param relation 新增对象。 + * @param slaveSqlTable 新增的关联从数据表对象。 + * @param slaveSqlColumn 新增的关联从数据表对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDatasourceRelation saveNew( + OnlineDatasourceRelation relation, SqlTable slaveSqlTable, SqlTableColumn slaveSqlColumn) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(relation.getDatasourceId())); + // 查找数据源关联的数据表,判断当前关联的从表,是否已经存在于zz_online_datasource_table中了。 + // 对于同一个数据源及其关联,同一个数据表只会被创建一次,如果已经和当前数据源的其他Relation, + // 作为从表绑定了,怎么就可以直接使用这个OnlineTable了,否则就会为这个SqlTable,创建对应的OnlineTable。 + List datasourceTableList = + onlineTableService.getOnlineTableListByDatasourceId(relation.getDatasourceId()); + OnlineTable relationSlaveTable = null; + OnlineColumn relationSlaveColumn = null; + for (OnlineTable onlineTable : datasourceTableList) { + if (onlineTable.getTableName().equals(slaveSqlTable.getTableName())) { + relationSlaveTable = onlineTable; + relationSlaveColumn = onlineColumnService.getOnlineColumnByTableIdAndColumnName( + onlineTable.getTableId(), slaveSqlColumn.getColumnName()); + break; + } + } + if (relationSlaveTable == null) { + relationSlaveTable = onlineTableService.saveNewFromSqlTable(slaveSqlTable); + for (OnlineColumn onlineColumn : relationSlaveTable.getColumnList()) { + if (onlineColumn.getColumnName().equals(slaveSqlColumn.getColumnName())) { + relationSlaveColumn = onlineColumn; + break; + } + } + } + TokenData tokenData = TokenData.takeFromRequest(); + relation.setRelationId(idGenerator.nextLongId()); + relation.setAppCode(tokenData.getAppCode()); + relation.setSlaveTableId(relationSlaveTable.getTableId()); + relation.setSlaveColumnId(relationSlaveColumn == null ? null : relationSlaveColumn.getColumnId()); + Date now = new Date(); + relation.setUpdateTime(now); + relation.setCreateTime(now); + relation.setCreateUserId(tokenData.getUserId()); + relation.setUpdateUserId(tokenData.getUserId()); + onlineDatasourceRelationMapper.insert(relation); + OnlineDatasourceTable datasourceTable = new OnlineDatasourceTable(); + datasourceTable.setId(idGenerator.nextLongId()); + datasourceTable.setDatasourceId(relation.getDatasourceId()); + datasourceTable.setRelationId(relation.getRelationId()); + datasourceTable.setTableId(relation.getSlaveTableId()); + onlineDatasourceTableMapper.insert(datasourceTable); + return relation; + } + + /** + * 更新数据对象。 + * + * @param relation 更新的对象。 + * @param originalRelation 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDatasourceRelation relation, OnlineDatasourceRelation originalRelation) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(relation.getDatasourceId())); + TokenData tokenData = TokenData.takeFromRequest(); + relation.setAppCode(tokenData.getAppCode()); + relation.setUpdateTime(new Date()); + relation.setUpdateUserId(tokenData.getUserId()); + relation.setCreateTime(originalRelation.getCreateTime()); + relation.setCreateUserId(originalRelation.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return onlineDatasourceRelationMapper.update(relation, false) == 1; + } + + /** + * 删除指定数据。 + * + * @param relationId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long relationId) { + OnlineDatasourceRelation relation = this.getById(relationId); + if (relation != null) { + commonRedisUtil.evictFormCache( + OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(relation.getDatasourceId())); + } + if (onlineDatasourceRelationMapper.deleteById(relationId) != 1) { + return false; + } + OnlineDatasourceTable filter = new OnlineDatasourceTable(); + filter.setRelationId(relationId); + QueryWrapper queryWrapper = QueryWrapper.create(filter); + OnlineDatasourceTable datasourceTable = onlineDatasourceTableMapper.selectOneByQuery(queryWrapper); + onlineDatasourceTableMapper.deleteByQuery(queryWrapper); + filter = new OnlineDatasourceTable(); + filter.setDatasourceId(datasourceTable.getDatasourceId()); + filter.setTableId(datasourceTable.getTableId()); + // 不在有引用该表的时候,可以删除该数据源关联引用的从表了。 + if (onlineDatasourceTableMapper.selectCountByQuery(QueryWrapper.create(filter)) == 0) { + onlineTableService.remove(datasourceTable.getTableId()); + } + return true; + } + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param datasourceId 主表主键Id。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int removeByDatasourceId(Long datasourceId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(datasourceId)); + return onlineDatasourceRelationMapper.deleteByQuery( + new QueryWrapper().eq(OnlineDatasourceRelation::getDatasourceId, datasourceId)); + } + + @Override + public List getOnlineDatasourceRelationListFromCache(Set datasourceIdSet) { + List resultList = new LinkedList<>(); + datasourceIdSet.forEach(datasourceId -> { + String key = OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(datasourceId); + RBucket bucket = redissonClient.getBucket(key); + if (bucket.isExists()) { + resultList.addAll(JSONArray.parseArray(bucket.get(), OnlineDatasourceRelation.class)); + } else { + OnlineDatasourceRelation filter = new OnlineDatasourceRelation(); + filter.setDatasourceId(datasourceId); + List relationList = this.getListByFilter(filter); + if (CollUtil.isNotEmpty(relationList)) { + resultList.addAll(relationList); + bucket.set(JSONArray.toJSONString(relationList)); + } + } + }); + return resultList; + } + + @Override + public OnlineDatasourceRelation getOnlineDatasourceRelationFromCache(Long datasourceId, Long relationId) { + List relationList = + this.getOnlineDatasourceRelationListFromCache(CollUtil.newHashSet(datasourceId)); + if (CollUtil.isEmpty(relationList)) { + return null; + } + return relationList.stream().filter(r -> r.getRelationId().equals(relationId)).findFirst().orElse(null); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceRelationList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceRelationListWithRelation( + OnlineDatasourceRelation filter, String orderBy) { + if (filter == null) { + filter = new OnlineDatasourceRelation(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + List resultList = + onlineDatasourceRelationMapper.getOnlineDatasourceRelationList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param relation 最新数据对象。 + * @param originalRelation 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData( + OnlineDatasourceRelation relation, OnlineDatasourceRelation originalRelation) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + if (this.needToVerify(relation, originalRelation, OnlineDatasourceRelation::getMasterColumnId) + && !onlineColumnService.existId(relation.getMasterColumnId())) { + return CallResult.error(String.format(errorMessageFormat, "主表关联字段Id")); + } + if (this.needToVerify(relation, originalRelation, OnlineDatasourceRelation::getSlaveTableId) + && !onlineTableService.existId(relation.getSlaveTableId())) { + return CallResult.error(String.format(errorMessageFormat, "从表Id")); + } + if (this.needToVerify(relation, originalRelation, OnlineDatasourceRelation::getSlaveColumnId) + && !onlineColumnService.existId(relation.getSlaveColumnId())) { + return CallResult.error(String.format(errorMessageFormat, "从表关联字段Id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java new file mode 100644 index 00000000..f45b22ef --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java @@ -0,0 +1,266 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineDatasourceMapper; +import com.orangeforms.common.online.dao.OnlineDatasourceTableMapper; +import com.orangeforms.common.online.dao.OnlinePageDatasourceMapper; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceTable; +import com.orangeforms.common.online.model.OnlinePageDatasource; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.github.pagehelper.Page; +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.*; +import java.util.stream.Collectors; + +/** + * 数据模型数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDatasourceService") +public class OnlineDatasourceServiceImpl extends BaseService implements OnlineDatasourceService { + + @Autowired + private OnlineDatasourceMapper onlineDatasourceMapper; + @Autowired + private OnlinePageDatasourceMapper onlinePageDatasourceMapper; + @Autowired + private OnlineDatasourceTableMapper onlineDatasourceTableMapper; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDatasourceMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineDatasource 新增对象。 + * @param sqlTable 新增的数据表对象。 + * @param pageId 关联的页面Id。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDatasource saveNew(OnlineDatasource onlineDatasource, SqlTable sqlTable, Long pageId) { + TokenData tokenData = TokenData.takeFromRequest(); + OnlineTable onlineTable = onlineTableService.saveNewFromSqlTable(sqlTable); + onlineDatasource.setDatasourceId(idGenerator.nextLongId()); + onlineDatasource.setAppCode(tokenData.getAppCode()); + onlineDatasource.setMasterTableId(onlineTable.getTableId()); + Date now = new Date(); + onlineDatasource.setUpdateTime(now); + onlineDatasource.setCreateTime(now); + onlineDatasource.setCreateUserId(tokenData.getUserId()); + onlineDatasource.setUpdateUserId(tokenData.getUserId()); + onlineDatasourceMapper.insert(onlineDatasource); + OnlineDatasourceTable datasourceTable = new OnlineDatasourceTable(); + datasourceTable.setId(idGenerator.nextLongId()); + datasourceTable.setDatasourceId(onlineDatasource.getDatasourceId()); + datasourceTable.setTableId(onlineDatasource.getMasterTableId()); + onlineDatasourceTableMapper.insert(datasourceTable); + OnlinePageDatasource onlinePageDatasource = new OnlinePageDatasource(); + onlinePageDatasource.setId(idGenerator.nextLongId()); + onlinePageDatasource.setPageId(pageId); + onlinePageDatasource.setDatasourceId(onlineDatasource.getDatasourceId()); + onlinePageDatasourceMapper.insert(onlinePageDatasource); + return onlineDatasource; + } + + /** + * 更新数据对象。 + * + * @param onlineDatasource 更新的对象。 + * @param originalOnlineDatasource 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDatasource onlineDatasource, OnlineDatasource originalOnlineDatasource) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceKey(onlineDatasource.getDatasourceId())); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDatasource.setAppCode(tokenData.getAppCode()); + onlineDatasource.setUpdateTime(new Date()); + onlineDatasource.setUpdateUserId(tokenData.getUserId()); + onlineDatasource.setCreateTime(originalOnlineDatasource.getCreateTime()); + onlineDatasource.setCreateUserId(originalOnlineDatasource.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return onlineDatasourceMapper.update(onlineDatasource, false) == 1; + } + + /** + * 删除指定数据。 + * + * @param datasourceId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long datasourceId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceKey(datasourceId)); + if (onlineDatasourceMapper.deleteById(datasourceId) == 0) { + return false; + } + onlineDatasourceRelationService.removeByDatasourceId(datasourceId); + // 开始删除多对多父表的关联 + OnlinePageDatasource onlinePageDatasource = new OnlinePageDatasource(); + onlinePageDatasource.setDatasourceId(datasourceId); + onlinePageDatasourceMapper.deleteByQuery(QueryWrapper.create(onlinePageDatasource)); + OnlineDatasourceTable filter = new OnlineDatasourceTable(); + filter.setDatasourceId(datasourceId); + QueryWrapper queryWrapper = QueryWrapper.create(filter); + List datasourceTableList = onlineDatasourceTableMapper.selectListByQuery(queryWrapper); + onlineDatasourceTableMapper.deleteByQuery(queryWrapper); + Set tableIdSet = datasourceTableList.stream() + .map(OnlineDatasourceTable::getTableId).collect(Collectors.toSet()); + onlineTableService.removeByTableIdSet(tableIdSet); + return true; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDatasourceListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceList(OnlineDatasource filter, String orderBy) { + if (filter == null) { + filter = new OnlineDatasource(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineDatasourceMapper.getOnlineDatasourceList(filter, orderBy); + } + + @Override + public OnlineDatasource getOnlineDatasourceFromCache(Long datasourceId) { + String key = OnlineRedisKeyUtil.makeOnlineDataSourceKey(datasourceId); + return commonRedisUtil.getFromCache(key, datasourceId, this::getById, OnlineDatasource.class); + } + + @Override + public List getOnlineDatasourceListFromCache(Set datasourceIdSet) { + List resultList = new LinkedList<>(); + datasourceIdSet.forEach(datasourceId -> resultList.add(this.getOnlineDatasourceFromCache(datasourceId))); + return resultList; + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceListWithRelation(OnlineDatasource filter, String orderBy) { + List resultList = this.getOnlineDatasourceList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param pageId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceListByPageId(Long pageId, OnlineDatasource filter, String orderBy) { + List resultList = + onlineDatasourceMapper.getOnlineDatasourceListByPageId(pageId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + /** + * 获取指定数据源Id集合所关联的在线表关联数据。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 数据源和数据表的多对多关联列表。 + */ + @Override + public List getOnlineDatasourceTableList(Set datasourceIdSet) { + return onlineDatasourceTableMapper.selectListByQuery( + new QueryWrapper().in(OnlineDatasourceTable::getDatasourceId, datasourceIdSet)); + } + + /** + * 根据在线表单Id集合,获取关联的在线数据源对象列表。 + * + * @param formIdSet 在线表单Id集合。 + * @return 与参数表单Id关联的数据源列表。 + */ + @Override + public List getOnlineDatasourceListByFormIds(Set formIdSet) { + return onlineDatasourceMapper.getOnlineDatasourceListByFormIds(formIdSet); + } + + @Override + public OnlineDatasource getOnlineDatasourceByMasterTableId(Long masterTableId) { + return onlineDatasourceMapper.selectOneByQuery( + new QueryWrapper().eq(OnlineDatasource::getMasterTableId, masterTableId)); + } + + @Override + public boolean existByVariableName(String variableName) { + OnlineDatasource filter = new OnlineDatasource(); + filter.setVariableName(variableName); + return CollUtil.isNotEmpty(this.getOnlineDatasourceList(filter, null)); + } + + @Override + public Map getPageIdAndVariableNameMapByPageIds(Set pageIds) { + String ids = CollUtil.join(pageIds, ","); + List> dataList = onlineDatasourceMapper.getPageIdAndVariableNameMapByPageIds(ids); + return dataList.stream() + .collect(Collectors.toMap(c -> (Long) c.get("page_id"), c -> (String) c.get("variable_name"))); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java new file mode 100644 index 00000000..d9369859 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java @@ -0,0 +1,201 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.constant.DblinkType; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dao.OnlineDblinkMapper; +import com.orangeforms.common.online.model.OnlineDblink; +import com.orangeforms.common.online.service.OnlineDblinkService; +import com.orangeforms.common.online.util.OnlineDataSourceUtil; +import com.github.pagehelper.Page; +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.*; +import java.util.stream.Collectors; + +/** + * 数据库链接数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDblinkService") +public class OnlineDblinkServiceImpl extends BaseService implements OnlineDblinkService { + + @Autowired + private OnlineDblinkMapper onlineDblinkMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDblinkMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDblink saveNew(OnlineDblink onlineDblink) { + onlineDblinkMapper.insert(this.buildDefaultValue(onlineDblink)); + return onlineDblink; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDblink onlineDblink, OnlineDblink originalOnlineDblink) { + if (!StrUtil.equals(onlineDblink.getConfiguration(), originalOnlineDblink.getConfiguration())) { + dataSourceUtil.removeDataSource(onlineDblink.getDblinkId()); + } + onlineDblink.setAppCode(TokenData.takeFromRequest().getAppCode()); + onlineDblink.setCreateUserId(originalOnlineDblink.getCreateUserId()); + onlineDblink.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + onlineDblink.setCreateTime(originalOnlineDblink.getCreateTime()); + onlineDblink.setUpdateTime(new Date()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return onlineDblinkMapper.update(onlineDblink, false) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dblinkId) { + dataSourceUtil.removeDataSource(dblinkId); + return onlineDblinkMapper.deleteById(dblinkId) == 1; + } + + @Override + public List getOnlineDblinkList(OnlineDblink filter, String orderBy) { + if (filter == null) { + filter = new OnlineDblink(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineDblinkMapper.getOnlineDblinkList(filter, orderBy); + } + + @Override + public List getOnlineDblinkListWithRelation(OnlineDblink filter, String orderBy) { + List resultList = this.getOnlineDblinkList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public List getDblinkTableList(OnlineDblink dblink) { + List resultList = dataSourceUtil.getTableList(dblink.getDblinkId(), null); + if (StrUtil.isNotBlank(onlineProperties.getTablePrefix())) { + resultList = resultList.stream() + .filter(t -> StrUtil.startWith(t.getTableName(), onlineProperties.getTablePrefix())) + .collect(Collectors.toList()); + } + resultList.forEach(t -> t.setDblinkId(dblink.getDblinkId())); + return resultList; + } + + @Override + public SqlTable getDblinkTable(OnlineDblink dblink, String tableName) { + SqlTable sqlTable = dataSourceUtil.getTable(dblink.getDblinkId(), tableName); + sqlTable.setDblinkId(dblink.getDblinkId()); + sqlTable.setColumnList(getDblinkTableColumnList(dblink, tableName)); + return sqlTable; + } + + @Override + public List getDblinkTableColumnList(OnlineDblink dblink, String tableName) { + List columnList = dataSourceUtil.getTableColumnList(dblink.getDblinkId(), tableName); + columnList.forEach(c -> this.makeupSqlTableColumn(c, dblink.getDblinkType())); + return columnList; + } + + @Override + public SqlTableColumn getDblinkTableColumn(OnlineDblink dblink, String tableName, String columnName) { + List columnList = dataSourceUtil.getTableColumnList(dblink.getDblinkId(), tableName); + SqlTableColumn sqlTableColumn = columnList.stream() + .filter(c -> c.getColumnName().equals(columnName)).findFirst().orElse(null); + if (sqlTableColumn != null) { + this.makeupSqlTableColumn(sqlTableColumn, dblink.getDblinkType()); + } + return sqlTableColumn; + } + + private void makeupSqlTableColumn(SqlTableColumn sqlTableColumn, int dblinkType) { + sqlTableColumn.setDblinkType(dblinkType); + switch (dblinkType) { + case DblinkType.POSTGRESQL: + case DblinkType.OPENGAUSS: + if (StrUtil.equalsAny(sqlTableColumn.getColumnType(), "char", "varchar")) { + sqlTableColumn.setFullColumnType( + sqlTableColumn.getColumnType() + "(" + sqlTableColumn.getStringPrecision() + ")"); + } else { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType()); + } + break; + case DblinkType.MYSQL: + sqlTableColumn.setAutoIncrement("auto_increment".equals(sqlTableColumn.getExtra())); + break; + case DblinkType.ORACLE: + if (StrUtil.equalsAny(sqlTableColumn.getColumnType(), "VARCHAR2", "NVARCHAR2", "CHAR", "NCHAR")) { + sqlTableColumn.setFullColumnType( + sqlTableColumn.getColumnType() + "(" + sqlTableColumn.getStringPrecision() + ")"); + } else if (StrUtil.equals(sqlTableColumn.getColumnType(), "NUMBER")) { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType() + + "(" + sqlTableColumn.getNumericPrecision() + "," + sqlTableColumn.getNumericScale() + ")"); + } else { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType()); + } + break; + case DblinkType.DAMENG: + case DblinkType.KINGBASE: + if (StrUtil.equalsAnyIgnoreCase(sqlTableColumn.getColumnType(), "VARCHAR", "VARCHAR2", "CHAR")) { + sqlTableColumn.setFullColumnType( + sqlTableColumn.getColumnType() + "(" + sqlTableColumn.getStringPrecision() + ")"); + } else if (StrUtil.equals(sqlTableColumn.getColumnType(), "NUMBER")) { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType() + + "(" + sqlTableColumn.getNumericPrecision() + "," + sqlTableColumn.getNumericScale() + ")"); + } else { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType()); + } + break; + default: + break; + } + } + + private OnlineDblink buildDefaultValue(OnlineDblink onlineDblink) { + onlineDblink.setDblinkId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDblink.setCreateUserId(tokenData.getUserId()); + onlineDblink.setUpdateUserId(tokenData.getUserId()); + Date now = new Date(); + onlineDblink.setCreateTime(now); + onlineDblink.setUpdateTime(now); + onlineDblink.setAppCode(tokenData.getAppCode()); + return onlineDblink; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java new file mode 100644 index 00000000..68c55d13 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java @@ -0,0 +1,187 @@ +package com.orangeforms.common.online.service.impl; + +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.orangeforms.common.online.dao.OnlineDictMapper; +import com.orangeforms.common.online.model.OnlineDict; +import com.orangeforms.common.online.service.OnlineDblinkService; +import com.orangeforms.common.online.service.OnlineDictService; +import com.github.pagehelper.Page; +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.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * 在线表单字典数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDictService") +public class OnlineDictServiceImpl extends BaseService implements OnlineDictService { + + @Autowired + private OnlineDictMapper onlineDictMapper; + @Autowired + private OnlineDblinkService dblinkService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDictMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineDict 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDict saveNew(OnlineDict onlineDict) { + onlineDict.setDictId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDict.setAppCode(tokenData.getAppCode()); + Date now = new Date(); + onlineDict.setUpdateTime(now); + onlineDict.setCreateTime(now); + onlineDict.setCreateUserId(tokenData.getUserId()); + onlineDict.setUpdateUserId(tokenData.getUserId()); + onlineDictMapper.insert(onlineDict); + return onlineDict; + } + + /** + * 更新数据对象。 + * + * @param onlineDict 更新的对象。 + * @param originalOnlineDict 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDict onlineDict, OnlineDict originalOnlineDict) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDictKey(onlineDict.getDictId())); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDict.setAppCode(tokenData.getAppCode()); + onlineDict.setUpdateTime(new Date()); + onlineDict.setUpdateUserId(tokenData.getUserId()); + onlineDict.setCreateTime(originalOnlineDict.getCreateTime()); + onlineDict.setCreateUserId(originalOnlineDict.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return onlineDictMapper.update(onlineDict, false) == 1; + } + + /** + * 删除指定数据。 + * + * @param dictId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dictId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDictKey(dictId)); + return onlineDictMapper.deleteById(dictId) == 1; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDictListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDictList(OnlineDict filter, String orderBy) { + if (filter == null) { + filter = new OnlineDict(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineDictMapper.getOnlineDictList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDictList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDictListWithRelation(OnlineDict filter, String orderBy) { + List resultList = this.getOnlineDictList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public OnlineDict getOnlineDictFromCache(Long dictId) { + String key = OnlineRedisKeyUtil.makeOnlineDictKey(dictId); + return commonRedisUtil.getFromCache(key, dictId, this::getById, OnlineDict.class); + } + + @Override + public List getOnlineDictListFromCache(Set dictIdSet) { + List dictList = new LinkedList<>(); + dictIdSet.forEach(dictId -> { + OnlineDict dict = this.getOnlineDictFromCache(dictId); + if (dict != null) { + dictList.add(dict); + } + }); + return dictList; + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param onlineDict 最新数据对象。 + * @param originalOnlineDict 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData(OnlineDict onlineDict, OnlineDict originalOnlineDict) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + //这里是基于字典的验证。 + if (this.needToVerify(onlineDict, originalOnlineDict, OnlineDict::getDblinkId) + && !dblinkService.existId(onlineDict.getDblinkId())) { + return CallResult.error(String.format(errorMessageFormat, "数据库链接主键id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java new file mode 100644 index 00000000..bf83cae9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java @@ -0,0 +1,306 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineFormDatasourceMapper; +import com.orangeforms.common.online.dao.OnlineFormMapper; +import com.orangeforms.common.online.model.OnlineForm; +import com.orangeforms.common.online.model.OnlineFormDatasource; +import com.orangeforms.common.online.service.OnlineFormService; +import com.orangeforms.common.online.service.OnlinePageService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.github.pagehelper.Page; +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.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线表单数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineFormService") +public class OnlineFormServiceImpl extends BaseService implements OnlineFormService { + + @Autowired + private OnlineFormMapper onlineFormMapper; + @Autowired + private OnlineFormDatasourceMapper onlineFormDatasourceMapper; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private RedissonClient redissonClient; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineFormMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineForm 新增对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineForm saveNew(OnlineForm onlineForm, Set datasourceIdSet) { + onlineForm.setFormId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + onlineForm.setAppCode(tokenData.getAppCode()); + onlineForm.setTenantId(tokenData.getTenantId()); + Date now = new Date(); + onlineForm.setUpdateTime(now); + onlineForm.setCreateTime(now); + onlineForm.setCreateUserId(tokenData.getUserId()); + onlineForm.setUpdateUserId(tokenData.getUserId()); + onlineFormMapper.insert(onlineForm); + if (CollUtil.isNotEmpty(datasourceIdSet)) { + for (Long datasourceId : datasourceIdSet) { + OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource(); + onlineFormDatasource.setId(idGenerator.nextLongId()); + onlineFormDatasource.setFormId(onlineForm.getFormId()); + onlineFormDatasource.setDatasourceId(datasourceId); + onlineFormDatasourceMapper.insert(onlineFormDatasource); + } + } + return onlineForm; + } + + /** + * 更新数据对象。 + * + * @param onlineForm 更新的对象。 + * @param originalOnlineForm 原有数据对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineForm onlineForm, OnlineForm originalOnlineForm, Set datasourceIdSet) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(onlineForm.getFormId())); + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(onlineForm.getFormId())); + TokenData tokenData = TokenData.takeFromRequest(); + onlineForm.setAppCode(tokenData.getAppCode()); + onlineForm.setTenantId(tokenData.getTenantId()); + onlineForm.setUpdateTime(new Date()); + onlineForm.setUpdateUserId(tokenData.getUserId()); + onlineForm.setCreateTime(originalOnlineForm.getCreateTime()); + onlineForm.setCreateUserId(originalOnlineForm.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + if (onlineFormMapper.update(onlineForm, false) != 1) { + return false; + } + OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource(); + formDatasourceFilter.setFormId(onlineForm.getFormId()); + onlineFormDatasourceMapper.deleteByQuery(QueryWrapper.create(formDatasourceFilter)); + if (CollUtil.isNotEmpty(datasourceIdSet)) { + for (Long datasourceId : datasourceIdSet) { + OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource(); + onlineFormDatasource.setId(idGenerator.nextLongId()); + onlineFormDatasource.setFormId(onlineForm.getFormId()); + onlineFormDatasource.setDatasourceId(datasourceId); + onlineFormDatasourceMapper.insert(onlineFormDatasource); + } + } + return true; + } + + /** + * 删除指定数据。 + * + * @param formId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long formId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(formId)); + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId)); + if (onlineFormMapper.deleteById(formId) != 1) { + return false; + } + OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource(); + formDatasourceFilter.setFormId(formId); + onlineFormDatasourceMapper.deleteByQuery(QueryWrapper.create(formDatasourceFilter)); + return true; + } + + /** + * 根据PageId,删除其所属的所有表单,以及表单关联的数据源数据。 + * + * @param pageId 指定的pageId。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int removeByPageId(Long pageId) { + OnlineForm filter = new OnlineForm(); + filter.setPageId(pageId); + List formList = onlineFormMapper.selectListByQuery(QueryWrapper.create(filter)); + Set formIdSet = formList.stream().map(OnlineForm::getFormId).collect(Collectors.toSet()); + if (CollUtil.isNotEmpty(formIdSet)) { + onlineFormDatasourceMapper.deleteByQuery(new QueryWrapper().in(OnlineFormDatasource::getFormId, formIdSet)); + for (Long formId : formIdSet) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(formId)); + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId)); + } + } + return onlineFormMapper.deleteByQuery(QueryWrapper.create(filter)); + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineFormListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineFormList(OnlineForm filter, String orderBy) { + if (filter == null) { + filter = new OnlineForm(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlineFormMapper.getOnlineFormList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineFormList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineFormListWithRelation(OnlineForm filter, String orderBy) { + List resultList = this.getOnlineFormList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 获取使用指定数据表的表单列表。 + * + * @param tableId 数据表Id。 + * @return 使用该数据表的表单列表。 + */ + @Override + public List getOnlineFormListByTableId(Long tableId) { + OnlineForm filter = new OnlineForm(); + filter.setMasterTableId(tableId); + return this.getOnlineFormList(filter, null); + } + + @Override + public List getFormDatasourceListFromCache(Long formId) { + String key = OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId); + RBucket bucket = redissonClient.getBucket(key); + if (bucket.isExists()) { + return JSONArray.parseArray(bucket.get(), OnlineFormDatasource.class); + } + QueryWrapper queryWrapper = new QueryWrapper().eq(OnlineFormDatasource::getFormId, formId); + List resultList = onlineFormDatasourceMapper.selectListByQuery(queryWrapper); + bucket.set(JSONArray.toJSONString(resultList)); + return resultList; + } + + /** + * 查询正在使用当前数据源的表单。 + * + * @param datasourceId 数据源Id。 + * @return 正在使用当前数据源的表单列表。 + */ + @Override + public List getOnlineFormListByDatasourceId(Long datasourceId) { + OnlineForm filter = new OnlineForm(); + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlineFormMapper.getOnlineFormListByDatasourceId(datasourceId, filter); + } + + @Override + public OnlineForm getOnlineFormFromCache(Long formId) { + String key = OnlineRedisKeyUtil.makeOnlineFormKey(formId); + return commonRedisUtil.getFromCache(key, formId, this::getById, OnlineForm.class); + } + + @Override + public boolean existByFormCode(String formCode) { + OnlineForm filter = new OnlineForm(); + filter.setFormCode(formCode); + return CollUtil.isNotEmpty(this.getOnlineFormList(filter, null)); + } + + @Override + public List getOnlineFormListByPageIds(Set pageIdSet) { + return onlineFormMapper.selectListByQuery(new QueryWrapper().eq(OnlineForm::getPageId, pageIdSet)); + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param onlineForm 最新数据对象。 + * @param originalOnlineForm 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData(OnlineForm onlineForm, OnlineForm originalOnlineForm) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + //这里是基于字典的验证。 + if (this.needToVerify(onlineForm, originalOnlineForm, OnlineForm::getMasterTableId) + && !onlineTableService.existId(onlineForm.getMasterTableId())) { + return CallResult.error(String.format(errorMessageFormat, "表单主表id")); + } + //这里是一对多的验证 + if (this.needToVerify(onlineForm, originalOnlineForm, OnlineForm::getPageId) + && !onlinePageService.existId(onlineForm.getPageId())) { + return CallResult.error(String.format(errorMessageFormat, "页面id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java new file mode 100644 index 00000000..28898e0e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java @@ -0,0 +1,1757 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.TypeReference; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.exception.NoDataPermException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.annotation.MultiDatabaseWriteMethod; +import com.orangeforms.common.datafilter.config.DataFilterProperties; +import com.orangeforms.common.dbutil.constant.DblinkType; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.online.dao.OnlineOperationMapper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.util.*; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.*; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.object.ConstDictInfo; +import com.orangeforms.common.online.object.ColumnData; +import com.orangeforms.common.online.object.JoinTableInfo; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.exception.OnlineRuntimeException; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import lombok.Data; +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.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineOperationService") +public class OnlineOperationServiceImpl implements OnlineOperationService { + + @Autowired + private OnlineOperationMapper onlineOperationMapper; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineOperationHelper onlineOperationHelper; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private OnlineCustomExtFactory customExtFactory; + @Autowired + private GlobalDictService globalDictService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + @Autowired + private DataFilterProperties dataFilterProperties; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + private static final String DICT_MAP_SUFFIX = "DictMap"; + private static final String DICT_MAP_LIST_SUFFIX = "DictMapList"; + private static final String SELECT = "SELECT "; + private static final String FROM = " FROM "; + private static final String WHERE = " WHERE "; + private static final String AND = " AND "; + + /** + * 聚合返回数据中,聚合键的常量字段名。 + * 如select groupColumn grouped_key, max(aggregationColumn) aggregated_value。 + */ + private static final String KEY_NAME = "grouped_key"; + /** + * 聚合返回数据中,聚合值的常量字段名。 + * 如select groupColumn grouped_key, max(aggregationColumn) aggregated_value。 + */ + private static final String VALUE_NAME = "aggregated_value"; + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewBatch(OnlineTable table, List dataList) { + for (JSONObject data : dataList) { + this.saveNew(table, data); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public Object saveNew(OnlineTable table, JSONObject data) { + ResponseResult> columnDataListResult = + onlineOperationHelper.buildTableData(table, data, false, null); + if (!columnDataListResult.isSuccess()) { + throw new OnlineRuntimeException(columnDataListResult.getErrorMessage()); + } + List columnDataList = columnDataListResult.getData(); + String columnNames = this.makeColumnNames(columnDataList); + List columnValueList = new LinkedList<>(); + Object id = null; + // 这里逐个处理每一行数据,特别是非自增主键、createUserId、createTime、逻辑删除等特殊属性的字段。 + for (ColumnData columnData : columnDataList) { + this.makeupColumnValue(columnData); + if (BooleanUtil.isFalse(columnData.getColumn().getAutoIncrement())) { + columnValueList.add(columnData.getColumnValue()); + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey())) { + id = columnData.getColumnValue(); + // 这里必须补齐主键值到JSON对象,后面的从表关联字段值填充可能会用到该值。 + data.put(columnData.getColumn().getColumnName(), id); + } + } + } + onlineOperationMapper.insert(table.getTableName(), columnNames, columnValueList); + return id; + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public Object saveNewWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap) { + Object id = this.saveNew(masterTable, masterData); + if (slaveDataListMap == null) { + return id; + } + // 迭代多个关联列表。 + for (Map.Entry> entry : slaveDataListMap.entrySet()) { + Long masterColumnId = entry.getKey().getMasterColumnId(); + OnlineColumn masterColumn = masterTable.getColumnMap().get(masterColumnId); + Object columnValue = masterData.get(masterColumn.getColumnName()); + OnlineTable slaveTable = entry.getKey().getSlaveTable(); + OnlineColumn slaveColumn = slaveTable.getColumnMap().get(entry.getKey().getSlaveColumnId()); + // 迭代关联中的数据集合 + for (JSONObject slaveData : entry.getValue()) { + if (!slaveData.containsKey(slaveTable.getPrimaryKeyColumn().getColumnName())) { + slaveData.put(slaveColumn.getColumnName(), columnValue); + this.saveNew(slaveTable, slaveData); + } + } + } + return id; + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineTable table, JSONObject data) { + ResponseResult> columnDataListResult = + onlineOperationHelper.buildTableData(table, data, true, null); + if (!columnDataListResult.isSuccess()) { + throw new OnlineRuntimeException(columnDataListResult.getErrorMessage()); + } + List columnDataList = columnDataListResult.getData(); + String tableName = table.getTableName(); + List updateColumnList = new LinkedList<>(); + List filterList = new LinkedList<>(); + String dataId = null; + for (ColumnData columnData : columnDataList) { + this.makeupColumnValue(columnData); + // 对于以下几种类型的字段,忽略更新。 + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey()) + || ObjectUtil.equal(columnData.getColumn().getFieldKind(), FieldKind.LOGIC_DELETE)) { + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(tableName); + filter.setColumnName(columnData.getColumn().getColumnName()); + filter.setColumnValue(columnData.getColumnValue()); + filterList.add(filter); + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey())) { + dataId = columnData.getColumnValue().toString(); + } + continue; + } + if (!MyCommonUtil.equalsAny(columnData.getColumn().getFieldKind(), + FieldKind.CREATE_TIME, FieldKind.CREATE_USER_ID, FieldKind.CREATE_DEPT_ID, FieldKind.TENANT_FILTER)) { + updateColumnList.add(columnData); + } + } + if (CollUtil.isEmpty(updateColumnList)) { + return true; + } + String dataPermFilter = this.buildDataPermFilter(table); + return this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateColumn(OnlineTable table, String dataId, OnlineColumn column, T dataValue) { + List updateColumnList = new LinkedList<>(); + ColumnData updateColumnData = new ColumnData(); + updateColumnData.setColumn(column); + updateColumnData.setColumnValue(dataValue); + updateColumnList.add(updateColumnData); + List filterList = this.makeDefaultFilter(table, table.getPrimaryKeyColumn(), dataId); + String dataPermFilter = this.buildDataPermFilter(table); + return this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void updateWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Long datasourceId, + Map> slaveDataListMap) { + this.update(masterTable, masterData); + if (slaveDataListMap == null) { + return; + } + String masterDataId = masterData.get(masterTable.getPrimaryKeyColumn().getColumnName()).toString(); + for (Map.Entry> relationEntry : slaveDataListMap.entrySet()) { + Long relationId = relationEntry.getKey().getRelationId(); + this.updateRelationData( + masterTable, masterData, masterDataId, datasourceId, relationId, relationEntry.getValue()); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void updateRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + Long datasourceId, + Long relationId, + List slaveDataList) { + ResponseResult relationResult = + onlineOperationHelper.verifyAndGetRelation(datasourceId, relationId); + if (!relationResult.isSuccess()) { + throw new OnlineRuntimeException(relationResult.getErrorMessage()); + } + OnlineDatasourceRelation relation = relationResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + if (relation.getRelationType().equals(RelationType.ONE_TO_ONE)) { + JSONObject slaveData = null; + if (CollUtil.isNotEmpty(slaveDataList)) { + slaveData = slaveDataList.get(0); + } + this.saveNewOrUpdateOneToOneRelationData( + masterTable, masterData, masterDataId, slaveTable, slaveData, relation); + } else if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + if (slaveDataList == null) { + return; + } + this.saveNewOrUpdateOneToManyRelationData( + masterTable, masterData, masterDataId, slaveTable, slaveDataList, relation); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public boolean delete(OnlineTable table, List relationList, String dataId) { + List filterList = + this.makeDefaultFilter(table, table.getPrimaryKeyColumn(), dataId); + String dataPermFilter = this.buildDataPermFilter(table); + if (table.getLogicDeleteColumn() == null) { + if (this.doDelete(table, filterList, dataPermFilter) != 1) { + return false; + } + } else { + this.doLogicDelete(table, table.getPrimaryKeyColumn(), dataId, dataPermFilter); + } + if (CollUtil.isEmpty(relationList)) { + return true; + } + Map masterData = getMasterData(table, null, null, dataId); + for (OnlineDatasourceRelation relation : relationList) { + if (BooleanUtil.isFalse(relation.getCascadeDelete())) { + continue; + } + OnlineTable slaveTable = relation.getSlaveTable(); + OnlineColumn slaveColumn = + relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + String columnValue = dataId; + if (!relation.getMasterColumnId().equals(table.getPrimaryKeyColumn().getColumnId())) { + OnlineColumn relationMasterColumn = table.getColumnMap().get(relation.getMasterColumnId()); + columnValue = masterData.get(relationMasterColumn.getColumnName()).toString(); + } + List slaveFilterList = + this.makeDefaultFilter(relation.getSlaveTable(), slaveColumn, columnValue); + if (slaveTable.getLogicDeleteColumn() == null) { + this.doDelete(slaveTable, slaveFilterList, null); + } else { + this.doLogicDelete(slaveTable, slaveColumn, columnValue, null); + } + } + return true; + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteOneToManySlaveData( + OnlineTable table, OnlineColumn column, String columnValue, Set keptIdSet) { + List filterList = this.makeDefaultFilter(table, column, columnValue); + if (CollUtil.isNotEmpty(keptIdSet)) { + OnlineFilterDto keptIdSetFilter = new OnlineFilterDto(); + Set convertedIdSet = + onlineOperationHelper.convertToTypeValue(table.getPrimaryKeyColumn(), keptIdSet); + keptIdSetFilter.setColumnValueList(new HashSet<>(convertedIdSet)); + keptIdSetFilter.setTableName(table.getTableName()); + keptIdSetFilter.setColumnName(table.getPrimaryKeyColumn().getColumnName()); + keptIdSetFilter.setFilterType(FieldFilterType.NOT_IN_LIST_FILTER); + filterList.add(keptIdSetFilter); + } + if (table.getLogicDeleteColumn() == null) { + this.doDelete(table, filterList, null); + } else { + this.doLogicDelete(table, filterList, null); + } + } + + @Override + public boolean existId(OnlineTable table, String dataId) { + return this.getMasterData(table, null, null, dataId) != null; + } + + @Override + public Map getMasterData( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + String dataId) { + List filterList = + this.makeDefaultFilter(table, table.getPrimaryKeyColumn(), dataId); + // 组件表关联数据。 + List joinInfoList = this.makeJoinInfoList(table, oneToOneRelationList); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFieldsWithRelation(table, oneToOneRelationList); + String dataPermFilter = this.buildDataPermFilter(table); + this.normalizeFiltersSlaveTableAlias(oneToOneRelationList, filterList); + selectFields = this.normalizeSlaveTableAlias(oneToOneRelationList, selectFields); + MyPageData> pageData = this.getList( + table, joinInfoList, selectFields, filterList, dataPermFilter, null, null); + List> resultList = pageData.getDataList(); + this.buildDataListWithDict(resultList, table, oneToOneRelationList); + if (CollUtil.isEmpty(resultList)) { + return null; + } + if (CollUtil.isNotEmpty(allRelationList)) { + // 针对一对多和多对多关联,计算虚拟聚合字段。 + List toManyRelationList = allRelationList.stream() + .filter(r -> !r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + this.buildVirtualColumn(resultList, table, toManyRelationList); + } + this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); + return resultList.get(0); + } + + @Override + public Map getSlaveData(OnlineDatasourceRelation relation, String dataId) { + OnlineTable slaveTable = relation.getSlaveTable(); + List filterList = + this.makeDefaultFilter(slaveTable, slaveTable.getPrimaryKeyColumn(), dataId); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFields(slaveTable, null); + String dataPermFilter = this.buildDataPermFilter(slaveTable); + MyPageData> pageData = this.getList( + slaveTable, null, selectFields, filterList, dataPermFilter, null, null); + List> resultList = pageData.getDataList(); + this.buildDataListWithDict(resultList, slaveTable); + return CollUtil.isEmpty(resultList) ? null : resultList.get(0); + } + + @Override + public MyPageData> getMasterDataList( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + List filterList, + String orderBy, + MyPageParam pageParam) { + this.normalizeFilterList(table, oneToOneRelationList, filterList); + // 组件表关联数据。 + List joinInfoList = this.makeJoinInfoList(table, oneToOneRelationList); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFieldsWithRelation(table, oneToOneRelationList); + String dataPermFilter = this.buildDataPermFilter(table); + this.normalizeFiltersSlaveTableAlias(oneToOneRelationList, filterList); + selectFields = this.normalizeSlaveTableAlias(oneToOneRelationList, selectFields); + orderBy = this.normalizeSlaveTableAlias(oneToOneRelationList, orderBy); + MyPageData> pageData = + this.getList(table, joinInfoList, selectFields, filterList, dataPermFilter, orderBy, pageParam); + List> resultList = pageData.getDataList(); + this.buildDataListWithDict(resultList, table, oneToOneRelationList); + // 针对一对多和多对多关联,计算虚拟聚合字段。 + if (CollUtil.isNotEmpty(allRelationList)) { + List toManyRelationList = allRelationList.stream() + .filter(r -> !r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + this.buildVirtualColumn(resultList, table, toManyRelationList); + } + this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); + return pageData; + } + + @Override + public MyPageData> getSlaveDataList( + OnlineDatasourceRelation relation, List filterList, String orderBy, MyPageParam pageParam) { + OnlineTable slaveTable = relation.getSlaveTable(); + this.normalizeFilterList(slaveTable, null, filterList); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFields(slaveTable, null); + String dataPermFilter = this.buildDataPermFilter(slaveTable); + MyPageData> pageData = + this.getList(slaveTable, null, selectFields, filterList, dataPermFilter, orderBy, pageParam); + this.buildDataListWithDict(pageData.getDataList(), slaveTable); + return pageData; + } + + @Override + public List> getDictDataList(OnlineDict dict, List filterList) { + if (StrUtil.isNotBlank(dict.getDeletedColumnName())) { + if (filterList == null) { + filterList = new LinkedList<>(); + } + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setColumnName(dict.getDeletedColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + if (StrUtil.isNotBlank(dict.getTenantFilterColumnName())) { + if (filterList == null) { + filterList = new LinkedList<>(); + } + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setColumnName(dict.getTenantFilterColumnName()); + filter.setColumnValue(TokenData.takeFromRequest().getTenantId()); + filterList.add(filter); + } + String selectFields = this.makeDictSelectFields(dict, false); + String dataPermFilter = this.buildDataPermFilter( + dict.getTableName(), dict.getDeptFilterColumnName(), dict.getUserFilterColumnName()); + return this.getDictList(dict.getDblinkId(), dict.getTableName(), selectFields, filterList, dataPermFilter); + } + + @Override + public void buildDataListWithDict( + OnlineTable masterTable, List relationList, List> dataList) { + this.buildDataListWithDict(dataList, masterTable, relationList); + } + + @Override + public Map calculatePermData(Set menuFormIds, Set viewFormIds, Set editFormIds) { + Map> formMenuPermMap = new HashMap<>(menuFormIds.size()); + for (Long menuFormId : menuFormIds) { + formMenuPermMap.put(menuFormId, new HashSet<>()); + } + Set permCodeSet = new HashSet<>(10); + Set permUrlSet = new HashSet<>(10); + if (CollUtil.isNotEmpty(viewFormIds)) { + List datasourceList = + onlineDatasourceService.getOnlineDatasourceListByFormIds(viewFormIds); + for (OnlineDatasource datasource : datasourceList) { + permCodeSet.add(OnlineUtil.makeViewPermCode(datasource.getVariableName())); + Set permUrls = onlineProperties.getViewUrlList().stream() + .map(url -> url + datasource.getVariableName()).collect(Collectors.toSet()); + permUrlSet.addAll(permUrls); + datasource.getOnlineFormDatasourceList().forEach(formDatasource -> + formMenuPermMap.get(formDatasource.getFormId()).addAll(permUrls)); + } + } + if (CollUtil.isNotEmpty(editFormIds)) { + List datasourceList = + onlineDatasourceService.getOnlineDatasourceListByFormIds(editFormIds); + for (OnlineDatasource datasource : datasourceList) { + permCodeSet.add(OnlineUtil.makeEditPermCode(datasource.getVariableName())); + Set permUrls = onlineProperties.getEditUrlList().stream() + .map(url -> url + datasource.getVariableName()).collect(Collectors.toSet()); + permUrlSet.addAll(permUrls); + datasource.getOnlineFormDatasourceList().forEach(formDatasource -> + formMenuPermMap.get(formDatasource.getFormId()).addAll(permUrls)); + } + } + List onlineWhitelistUrls = CollUtil.newArrayList( + onlineProperties.getUrlPrefix() + "/onlineOperation/listDict", + onlineProperties.getUrlPrefix() + "/onlineForm/render", + onlineProperties.getUrlPrefix() + "/onlineForm/view"); + Map resultMap = new HashMap<>(3); + resultMap.put("permCodeSet", permCodeSet); + resultMap.put("permUrlSet", permUrlSet); + resultMap.put("formMenuPermMap", formMenuPermMap); + resultMap.put("onlineWhitelistUrls", onlineWhitelistUrls); + return resultMap; + } + + private boolean doUpdate( + OnlineTable table, List updateColumns, List filters, String dataPermFilter) { + return onlineOperationMapper.update(table.getTableName(), updateColumns, filters, dataPermFilter) == 1; + } + + private int doDelete(OnlineTable table, List filters, String dataPermFilter) { + return onlineOperationMapper.delete(table.getTableName(), filters, dataPermFilter); + } + + private List> getGroupedListByCondition( + Long dblinkId, String selectTable, String selectFields, String whereClause, String groupBy) { + return onlineOperationMapper.getGroupedListByCondition(selectTable, selectFields, whereClause, groupBy); + } + + private List> getDictList( + Long dblinkId, String tableName, String selectFields, List filterList, String dataPermFilter) { + return onlineOperationMapper.getDictList(tableName, selectFields, filterList, dataPermFilter); + } + + private MyPageData> getList( + OnlineTable table, + List joinInfoList, + String selectFields, + List filterList, + String dataPermFilter, + String orderBy, + MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + List> resultList = onlineOperationMapper.getList( + table.getTableName(), joinInfoList, selectFields, filterList, dataPermFilter, orderBy); + return MyPageUtil.makeResponseData(resultList); + } + + private String makeWhereClause(List filters, String dataPermFilter, List paramList) { + if (CollUtil.isEmpty(filters) && StrUtil.isBlank(dataPermFilter)) { + return ""; + } + StringBuilder where = new StringBuilder(512); + List normalizedFilters = new LinkedList<>(); + if (CollUtil.isNotEmpty(filters)) { + for (OnlineFilterDto filter : filters) { + String filterString = this.makeSubWhereClause(filter, paramList); + if (StrUtil.isNotBlank(filterString)) { + normalizedFilters.add(filterString); + } + } + } + if (CollUtil.isNotEmpty(normalizedFilters)) { + where.append(WHERE); + where.append(CollUtil.join(normalizedFilters, AND)); + } + if (StrUtil.isNotBlank(dataPermFilter)) { + if (CollUtil.isNotEmpty(normalizedFilters)) { + where.append(AND); + } else { + where.append(WHERE); + } + where.append(dataPermFilter); + } + return where.toString(); + } + + private String makeSubWhereClause(OnlineFilterDto filter, List paramList) { + StringBuilder where = new StringBuilder(256); + if (filter.getFilterType().equals(FieldFilterType.EQUAL_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" = ? "); + paramList.add(filter.getColumnValue()); + } else if (filter.getFilterType().equals(FieldFilterType.RANGE_FILTER)) { + where.append(this.makeRangeFilterClause(filter, paramList)); + } else if (filter.getFilterType().equals(FieldFilterType.LIKE_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" LIKE ? "); + paramList.add(filter.getColumnValue()); + } else if (filter.getFilterType().equals(FieldFilterType.IN_LIST_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" IN ( "); + where.append(StrUtil.repeat("?,", filter.getColumnValueList().size())); + where.setLength(where.length() - 1); + where.append(")"); + paramList.addAll(filter.getColumnValueList()); + } else if (filter.getFilterType().equals(FieldFilterType.MULTI_LIKE)) { + where.append("("); + StringBuilder sb = new StringBuilder(128); + sb.append(this.makeWhereLeftOperator(filter)).append(" LIKE ? OR "); + String s = StrUtil.repeat(sb.toString(), filter.getColumnValueList().size()); + where.append(s, 0, s.length() - 4); + where.append(")"); + paramList.addAll(filter.getColumnValueList()); + } else if (filter.getFilterType().equals(FieldFilterType.NOT_IN_LIST_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" NOT IN ("); + where.append(StrUtil.repeat("?,", filter.getColumnValueList().size())); + where.setLength(where.length() - 1); + where.append(")"); + paramList.addAll(filter.getColumnValueList()); + } else if (filter.getFilterType().equals(FieldFilterType.IS_NULL)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" IS NULL "); + } else if (filter.getFilterType().equals(FieldFilterType.IS_NOT_NULL)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" IS NOT NULL "); + } + return where.toString(); + } + + private String makeRangeFilterClause(OnlineFilterDto filter, List paramList) { + StringBuilder where = new StringBuilder(256); + if (ObjectUtil.isNotEmpty(filter.getColumnValueStart())) { + where.append(this.makeWhereLeftOperator(filter)); + if (BooleanUtil.isTrue(filter.getIsOracleDate())) { + where.append(" >= ").append(filter.getColumnValueStart()); + } else { + where.append(" >= ? "); + paramList.add(filter.getColumnValueStart()); + } + } + if (ObjectUtil.isNotEmpty(filter.getColumnValueEnd())) { + if (ObjectUtil.isNotEmpty(filter.getColumnValueStart())) { + where.append(AND); + } + where.append(this.makeWhereLeftOperator(filter)); + if (BooleanUtil.isTrue(filter.getIsOracleDate())) { + where.append(" <= ").append(filter.getColumnValueEnd()); + } else { + where.append(" <= ? "); + paramList.add(filter.getColumnValueEnd()); + } + } + return where.toString(); + } + + private String makeWhereLeftOperator(OnlineFilterDto filter) { + if (StrUtil.isBlank(filter.getTableName())) { + return filter.getColumnName(); + } + StringBuilder sb = new StringBuilder(128); + sb.append(filter.getTableName()).append(".").append(filter.getColumnName()); + return sb.toString(); + } + + private void saveNewOrUpdateOneToManyRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + OnlineTable slaveTable, + List relationDataList, + OnlineDatasourceRelation relation) { + if (masterData == null) { + masterData = this.getMasterData(masterTable, null, null, masterDataId); + } + Set idSet = new HashSet<>(relationDataList.size()); + for (JSONObject relationData : relationDataList) { + Object id = relationData.get(relation.getSlaveTable().getPrimaryKeyColumn().getColumnName()); + if (ObjectUtil.isNotEmpty(id)) { + idSet.add(id.toString()); + } + } + // 自动补齐主表关联数据。 + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + Object masterColumnValue = masterData.get(masterColumn.getColumnName()); + OnlineColumn slaveColumn = relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + // 在从表中删除本地批量更新不存在的数据。 + this.deleteOneToManySlaveData( + relation.getSlaveTable(), slaveColumn, masterColumnValue.toString(), idSet); + for (JSONObject relationData : relationDataList) { + // 自动补齐主表关联数据。 + relationData.put(slaveColumn.getColumnName(), masterColumnValue); + // 拆解主表和一对多关联从表的输入参数,并构建出数据表的待插入数据列表。 + Object id = relationData.get(relation.getSlaveTable().getPrimaryKeyColumn().getColumnName()); + if (id == null) { + this.saveNew(slaveTable, relationData); + } else { + this.update(slaveTable, relationData); + } + } + } + + private void saveNewOrUpdateOneToOneRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + OnlineTable slaveTable, + JSONObject slaveData, + OnlineDatasourceRelation relation) { + if (MapUtil.isEmpty(slaveData)) { + return; + } + String keyColumnName = slaveTable.getPrimaryKeyColumn().getColumnName(); + String slaveDataId = slaveData.getString(keyColumnName); + if (slaveDataId == null) { + if (masterData == null) { + masterData = this.getMasterData(masterTable, null, null, masterDataId); + } + // 自动补齐主表关联数据。 + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + Object masterColumnValue = masterData.get(masterColumn.getColumnName()); + OnlineColumn slaveColumn = slaveTable.getColumnMap().get(relation.getSlaveColumnId()); + slaveData.put(slaveColumn.getColumnName(), masterColumnValue); + this.saveNew(slaveTable, slaveData); + } else { + Map originalSlaveData = + this.getMasterData(slaveTable, null, null, slaveDataId); + for (Map.Entry entry : originalSlaveData.entrySet()) { + slaveData.putIfAbsent(entry.getKey(), entry.getValue()); + } + if (!this.update(slaveTable, slaveData)) { + throw new OnlineRuntimeException("关联从表 [" + slaveTable.getTableName() + "] 中的更新数据不存在"); + } + } + } + + private void reformatResultListWithOneToOneRelation( + List> resultList, List oneToOneRelationList) { + if (CollUtil.isEmpty(oneToOneRelationList) || CollUtil.isEmpty(resultList)) { + return; + } + for (OnlineDatasourceRelation r : oneToOneRelationList) { + for (Map resultMap : resultList) { + Collection slaveColumnList = r.getSlaveTable().getColumnMap().values(); + Map oneToOneRelationDataMap = new HashMap<>(slaveColumnList.size()); + resultMap.put(r.getVariableName(), oneToOneRelationDataMap); + for (OnlineColumn c : slaveColumnList) { + StringBuilder sb = new StringBuilder(64); + sb.append(r.getVariableName()) + .append(OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR).append(c.getColumnName()); + Object data = this.removeRelationColumnData(resultMap, sb.toString()); + oneToOneRelationDataMap.put(c.getColumnName(), data); + if (c.getDictId() != null) { + sb.append(DICT_MAP_SUFFIX); + data = this.removeRelationColumnData(resultMap, sb.toString()); + oneToOneRelationDataMap.put(c.getColumnName() + DICT_MAP_SUFFIX, data); + } + } + } + } + } + + private Object removeRelationColumnData(Map resultMap, String name) { + Object data = resultMap.remove(name); + if (data == null) { + data = resultMap.remove("\"" + name + "\""); + } + return data; + } + + private void buildVirtualColumn( + List> resultList, OnlineTable table, List relationList) { + if (CollUtil.isEmpty(resultList) || CollUtil.isEmpty(relationList)) { + return; + } + OnlineVirtualColumn virtualColumnFilter = new OnlineVirtualColumn(); + virtualColumnFilter.setTableId(table.getTableId()); + virtualColumnFilter.setVirtualType(VirtualType.AGGREGATION); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnList(virtualColumnFilter, null); + if (CollUtil.isEmpty(virtualColumnList)) { + return; + } + Map relationMap = + relationList.stream().collect(Collectors.toMap(OnlineDatasourceRelation::getRelationId, r -> r)); + for (OnlineVirtualColumn virtualColumn : virtualColumnList) { + OnlineDatasourceRelation relation = relationMap.get(virtualColumn.getRelationId()); + if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + this.doBuildVirtualColumnForOneToMany(table, resultList, virtualColumn, relation); + } + } + } + + private void doBuildVirtualColumnForOneToMany( + OnlineTable masterTable, + List> resultList, + OnlineVirtualColumn virtualColumn, + OnlineDatasourceRelation relation) { + String slaveTableName = relation.getSlaveTable().getTableName(); + OnlineColumn slaveColumn = + relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + String slaveColumnName = slaveColumn.getColumnName(); + OnlineColumn aggregationColumn = + relation.getSlaveTable().getColumnMap().get(virtualColumn.getAggregationColumnId()); + String aggregationColumnName = aggregationColumn.getColumnName(); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTableName, slaveColumnName, slaveTableName, aggregationColumnName, virtualColumn.getAggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + // 开始组装过滤从句。 + List criteriaList = new LinkedList<>(); + // 1. 组装主表数据对从表的过滤条件。 + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + String masterColumnName = masterColumn.getColumnName(); + Set masterIdSet = resultList.stream() + .map(r -> r.get(masterColumnName)).filter(Objects::nonNull).collect(Collectors.toSet()); + inlistFilter.setCriteria( + slaveTableName, slaveColumnName, slaveColumn.getObjectFieldType(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + // 2. 从表逻辑删除字段过滤。 + if (relation.getSlaveTable().getLogicDeleteColumn() != null) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + slaveTableName, + relation.getSlaveTable().getLogicDeleteColumn().getColumnName(), + relation.getSlaveTable().getLogicDeleteColumn().getObjectFieldType(), + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (StrUtil.isNotBlank(virtualColumn.getWhereClauseJson())) { + List whereClauseList = + JSONArray.parseArray(virtualColumn.getWhereClauseJson(), VirtualColumnWhereClause.class); + if (CollUtil.isNotEmpty(whereClauseList)) { + for (VirtualColumnWhereClause whereClause : whereClauseList) { + MyWhereCriteria whereClauseFilter = new MyWhereCriteria(); + OnlineColumn c = relation.getSlaveTable().getColumnMap().get(whereClause.getColumnId()); + whereClauseFilter.setCriteria( + slaveTableName, + c.getColumnName(), + c.getObjectFieldType(), + whereClause.getOperatorType(), + whereClause.getValue()); + criteriaList.add(whereClauseFilter); + } + } + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + List> aggregationMapList = + getGroupedListByCondition(masterTable.getDblinkId(), slaveTableName, selectList, criteriaString, groupBy); + this.doMakeAggregationData(resultList, aggregationMapList, masterColumnName, virtualColumn.getObjectFieldName()); + } + + private void doMakeAggregationData( + List> resultList, + List> aggregationMapList, + String masterColumnName, + String virtualColumnName) { + // 根据获取的分组聚合结果集,绑定到主表总的关联字段。 + if (CollUtil.isEmpty(aggregationMapList)) { + return; + } + Map relatedMap = new HashMap<>(aggregationMapList.size()); + for (Map map : aggregationMapList) { + relatedMap.put(map.get(KEY_NAME).toString(), map.get(VALUE_NAME)); + } + for (Map dataObject : resultList) { + String masterIdValue = dataObject.get(masterColumnName).toString(); + if (masterIdValue != null) { + Object value = relatedMap.get(masterIdValue); + if (value != null) { + dataObject.put(virtualColumnName, value); + } + } + } + } + + private Tuple2 makeSelectListAndGroupByClause( + String groupTableName, + String groupColumnName, + String aggregationTableName, + String aggregationColumnName, + Integer aggregationType) { + String aggregationFunc = AggregationType.getAggregationFunction(aggregationType); + // 构建Select List + // 如:r_table.master_id groupedKey, SUM(r_table.aggr_column) aggregated_value + StringBuilder groupedSelectList = new StringBuilder(128); + groupedSelectList.append(groupTableName) + .append(".") + .append(groupColumnName) + .append(" ") + .append(KEY_NAME) + .append(", ") + .append(aggregationFunc) + .append("(") + .append(aggregationTableName) + .append(".") + .append(aggregationColumnName) + .append(") ") + .append(VALUE_NAME) + .append(" "); + StringBuilder groupBy = new StringBuilder(64); + groupBy.append(groupTableName).append(".").append(groupColumnName); + return new Tuple2<>(groupedSelectList.toString(), groupBy.toString()); + } + + private void buildDataListWithDict(List> resultList, OnlineTable slaveTable) { + if (CollUtil.isEmpty(resultList)) { + return; + } + Set dictIdSet = new HashSet<>(); + // 先找主表字段对字典的依赖。 + Multimap dictColumnMap = LinkedHashMultimap.create(); + for (OnlineColumn column : slaveTable.getColumnMap().values()) { + if (column.getDictId() != null) { + dictIdSet.add(column.getDictId()); + column.setColumnAliasName(column.getColumnName()); + dictColumnMap.put(column.getDictId(), column); + } + } + this.doBuildDataListWithDict(resultList, dictIdSet, dictColumnMap); + } + + private void buildDataListWithDict( + List> resultList, + OnlineTable masterTable, + List relationList) { + if (CollUtil.isEmpty(resultList)) { + return; + } + Set dictIdSet = new HashSet<>(); + // 先找主表字段对字典的依赖。 + Multimap dictColumnMap = LinkedHashMultimap.create(); + for (OnlineColumn column : masterTable.getColumnMap().values()) { + if (column.getDictId() != null) { + dictIdSet.add(column.getDictId()); + column.setColumnAliasName(column.getColumnName()); + dictColumnMap.put(column.getDictId(), column); + } + } + // 再找关联表字段对字典的依赖。 + if (CollUtil.isEmpty(relationList)) { + this.doBuildDataListWithDict(resultList, dictIdSet, dictColumnMap); + return; + } + for (OnlineDatasourceRelation relation : relationList) { + for (OnlineColumn column : relation.getSlaveTable().getColumnMap().values()) { + if (column.getDictId() != null) { + dictIdSet.add(column.getDictId()); + String columnAliasName = relation.getVariableName() + + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR + column.getColumnName(); + column.setColumnAliasName(columnAliasName); + dictColumnMap.put(column.getDictId(), column); + } + } + } + this.doBuildDataListWithDict(resultList, dictIdSet, dictColumnMap); + } + + private void doBuildDataListWithDict( + List> resultList, Set dictIdSet, Multimap dictColumnMap) { + if (CollUtil.isEmpty(dictIdSet)) { + return; + } + List allDictList = onlineDictService.getOnlineDictListFromCache(dictIdSet); + for (OnlineDict dict : allDictList) { + Collection columnList = dictColumnMap.get(dict.getDictId()); + for (OnlineColumn column : columnList) { + Set dictIdDataSet = this.extractColumnDictIds(resultList, column); + if (CollUtil.isNotEmpty(dictIdDataSet)) { + this.doBindColumnDictData(resultList, column, dict, dictIdDataSet); + } + } + } + } + + private Set extractColumnDictValues(List> dataList, OnlineColumn column) { + Set dictValueDataSet = new HashSet<>(); + for (Map data : dataList) { + String dictValueData = (String) data.get(column.getColumnAliasName()); + if (StrUtil.isNotBlank(dictValueData)) { + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + Set dictValueDataList = StrUtil.split(dictValueData, ",") + .stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + CollUtil.addAll(dictValueDataSet, dictValueDataList); + } else { + dictValueDataSet.add(dictValueData); + } + } + } + return dictValueDataSet; + } + + private Set extractColumnDictIds(List> resultList, OnlineColumn column) { + Set dictIdDataSet = new HashSet<>(); + for (Map result : resultList) { + Object dictIdData = result.get(column.getColumnAliasName()); + if (ObjectUtil.isEmpty(dictIdData)) { + continue; + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + Set dictIdDataList = StrUtil.split(dictIdData.toString(), ",") + .stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + if (ObjectFieldType.LONG.equals(column.getObjectFieldType())) { + dictIdDataList = dictIdDataSet.stream() + .map(c -> (Serializable) Long.valueOf(c.toString())).collect(Collectors.toSet()); + } + CollUtil.addAll(dictIdDataSet, dictIdDataList); + } else { + dictIdDataSet.add((Serializable) dictIdData); + } + } + return dictIdDataSet; + } + + private Map getGlobalDictItemDictMapFromCache(String dictCode, Set itemIds) { + return globalDictService.getGlobalDictItemDictMapFromCache(dictCode, itemIds); + } + + private void doTranslateColumnDictData( + List> dataList, + OnlineColumn column, + OnlineDict dict, + Set dictValueDataSet) { + Map dictResultMap = this.doTranslateColumnDictDataMap(dict, dictValueDataSet); + for (Map data : dataList) { + String dictValueData = (String) data.get(column.getColumnAliasName()); + if (StrUtil.isBlank(dictValueData)) { + continue; + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + List dictValueDataList = StrUtil.splitTrim(dictValueData, ","); + List dictIdList = dictValueDataList.stream() + .map(dictResultMap::get).filter(Objects::nonNull).collect(Collectors.toList()); + data.put(column.getColumnAliasName(), CollUtil.join(dictIdList, ",")); + } else { + Object dictId = dictResultMap.get(dictValueData); + if (dictId != null) { + data.put(column.getColumnAliasName(), dictId); + } + } + } + } + + private Map doTranslateColumnDictDataMap(OnlineDict dict, Set dictValueDataSet) { + Map dictResultMap = new HashMap<>(dictValueDataSet.size()); + if (dict.getDictType().equals(DictType.CUSTOM)) { + ConstDictInfo dictInfo = + JSONObject.parseObject(dict.getDictDataJson(), ConstDictInfo.class); + List dictDataList = dictInfo.getDictData(); + for (ConstDictInfo.ConstDictData dictData : dictDataList) { + dictResultMap.put(dictData.getName(), dictData.getId()); + } + } else if (dict.getDictType().equals(DictType.GLOBAL_DICT)) { + Map dictDataMap = + this.getGlobalDictItemDictMapFromCache(dict.getDictCode(), null); + dictDataMap.entrySet().stream() + .filter(entry -> dictValueDataSet.contains(entry.getValue())) + .forEach(entry -> dictResultMap.put(entry.getValue(), entry.getKey())); + } else if (dict.getDictType().equals(DictType.TABLE)) { + String selectFields = this.makeDictSelectFields(dict, true); + List filterList = this.createDefaultFilter(dict); + OnlineFilterDto inlistFilter = new OnlineFilterDto(); + inlistFilter.setTableName(dict.getTableName()); + inlistFilter.setColumnName(dict.getValueColumnName()); + inlistFilter.setColumnValueList(dictValueDataSet); + inlistFilter.setFilterType(FieldFilterType.IN_LIST_FILTER); + filterList.add(inlistFilter); + List> dictResultList = + this.getDictList(dict.getDblinkId(), dict.getTableName(), selectFields, filterList, null); + if (CollUtil.isNotEmpty(dictResultList)) { + for (Map dictResult : dictResultList) { + dictResultMap.put(dictResult.get("name").toString(), dictResult.get("id")); + } + } + } else if (dict.getDictType().equals(DictType.URL)) { + this.buildUrlDictDataMap(dict, dictResultMap, false); + } + return dictResultMap; + } + + private Map doBuildColumnDictDataMap(OnlineDict dict, Set dictIdDataSet) { + Map dictResultMap = new HashMap<>(dictIdDataSet.size()); + if (dict.getDictType().equals(DictType.CUSTOM)) { + ConstDictInfo dictInfo = + JSONObject.parseObject(dict.getDictDataJson(), ConstDictInfo.class); + List dictDataList = dictInfo.getDictData(); + for (ConstDictInfo.ConstDictData dictData : dictDataList) { + dictResultMap.put(dictData.getId().toString(), dictData.getName()); + } + } else if (dict.getDictType().equals(DictType.GLOBAL_DICT)) { + Map dictDataMap = + this.getGlobalDictItemDictMapFromCache(dict.getDictCode(), dictIdDataSet); + for (Map.Entry entry : dictDataMap.entrySet()) { + dictResultMap.put(entry.getKey().toString(), entry.getValue()); + } + } else if (dict.getDictType().equals(DictType.TABLE)) { + String selectFields = this.makeDictSelectFields(dict, true); + List filterList = this.createDefaultFilter(dict); + OnlineFilterDto inlistFilter = new OnlineFilterDto(); + inlistFilter.setTableName(dict.getTableName()); + inlistFilter.setColumnName(dict.getKeyColumnName()); + inlistFilter.setColumnValueList(dictIdDataSet); + inlistFilter.setFilterType(FieldFilterType.IN_LIST_FILTER); + filterList.add(inlistFilter); + List> dictResultList = + this.getDictList(dict.getDblinkId(), dict.getTableName(), selectFields, filterList, null); + if (CollUtil.isNotEmpty(dictResultList)) { + for (Map dictResult : dictResultList) { + dictResultMap.put(dictResult.get("id").toString(), dictResult.get("name")); + } + } + } else if (dict.getDictType().equals(DictType.URL)) { + this.buildUrlDictDataMap(dict, dictResultMap, true); + } + return dictResultMap; + } + + private List createDefaultFilter(OnlineDict dict) { + List filterList = new LinkedList<>(); + if (StrUtil.isNotBlank(dict.getDeletedColumnName())) { + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(dict.getTableName()); + filter.setColumnName(dict.getDeletedColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + return filterList; + } + + private void buildUrlDictDataMap(OnlineDict dict, Map dictResultMap, boolean keyToValue) { + Map param = new HashMap<>(1); + param.put("Authorization", TokenData.takeFromRequest().getToken()); + String responseData = HttpUtil.get(dict.getDictListUrl(), param); + ResponseResult responseResult = + JSON.parseObject(responseData, new TypeReference>() { + }); + if (!responseResult.isSuccess()) { + throw new OnlineRuntimeException(responseResult.getErrorMessage()); + } + JSONArray dictDataArray = responseResult.getData(); + for (int i = 0; i < dictDataArray.size(); i++) { + JSONObject dictData = dictDataArray.getJSONObject(i); + if (keyToValue) { + dictResultMap.put(dictData.getString(dict.getKeyColumnName()), dictData.get(dict.getValueColumnName())); + } else { + dictResultMap.put(dictData.getString(dict.getValueColumnName()), dictData.get(dict.getKeyColumnName())); + } + } + } + + private void doBindColumnDictData( + List> resultList, + OnlineColumn column, + OnlineDict dict, + Set dictIdDataSet) { + Map dictResultMap = this.doBuildColumnDictDataMap(dict, dictIdDataSet); + String dictKeyName; + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + dictKeyName = column.getColumnAliasName() + DICT_MAP_LIST_SUFFIX; + } else { + dictKeyName = column.getColumnAliasName() + DICT_MAP_SUFFIX; + } + for (Map result : resultList) { + Object dictIdData = result.get(column.getColumnAliasName()); + if (ObjectUtil.isEmpty(dictIdData)) { + continue; + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + List dictIdDataList = StrUtil.splitTrim(dictIdData.toString(), ","); + List> dictMapList = new LinkedList<>(); + for (String data : dictIdDataList) { + Object dictNameData = dictResultMap.get(data); + Map dictMap = new HashMap<>(2); + dictMap.put("id", data); + dictMap.put("name", dictNameData); + dictMapList.add(dictMap); + } + result.put(dictKeyName, dictMapList); + } else { + Object dictNameData = dictResultMap.get(dictIdData.toString()); + Map dictMap = new HashMap<>(2); + dictMap.put("id", dictIdData); + dictMap.put("name", dictNameData); + result.put(dictKeyName, dictMap); + } + } + } + + private List makeJoinInfoList( + OnlineTable masterTable, List relationList) { + List joinInfoList = new LinkedList<>(); + if (CollUtil.isEmpty(relationList)) { + return joinInfoList; + } + Map masterTableColumnMap = masterTable.getColumnMap(); + for (OnlineDatasourceRelation relation : relationList) { + JoinTableInfo joinInfo = new JoinTableInfo(); + joinInfo.setLeftJoin(relation.getLeftJoin()); + joinInfo.setJoinTableName(relation.getSlaveTable().getTableName() + " " + relation.getVariableName()); + // 根据配置动态拼接JOIN的关联条件,同时要考虑从表的逻辑删除过滤。 + OnlineColumn masterColumn = masterTableColumnMap.get(relation.getMasterColumnId()); + OnlineColumn slaveColumn = relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + StringBuilder conditionBuilder = new StringBuilder(64); + conditionBuilder + .append(masterTable.getTableName()) + .append(".") + .append(masterColumn.getColumnName()) + .append(" = ") + .append(relation.getVariableName()) + .append(".") + .append(slaveColumn.getColumnName()); + if (relation.getSlaveTable().getLogicDeleteColumn() != null) { + conditionBuilder + .append(AND) + .append(relation.getVariableName()) + .append(".") + .append(relation.getSlaveTable().getLogicDeleteColumn().getColumnName()) + .append(" = ") + .append(GlobalDeletedFlag.NORMAL); + } + joinInfo.setJoinCondition(conditionBuilder.toString()); + joinInfoList.add(joinInfo); + } + return joinInfoList; + } + + private String makeSelectFields(OnlineTable table, String relationVariable) { + DataSourceProvider provider = dataSourceUtil.getProvider(table.getDblinkId()); + StringBuilder selectFieldBuider = new StringBuilder(512); + String intString = "SIGNED"; + if (provider.getDblinkType() == DblinkType.POSTGRESQL|| provider.getDblinkType() == DblinkType.OPENGAUSS) { + intString = "INT8"; + } + // 拼装主表的select fields字段。 + for (OnlineColumn column : table.getColumnMap().values()) { + OnlineColumn deletedColumn = table.getLogicDeleteColumn(); + String columnAliasName = column.getColumnName(); + if (relationVariable != null) { + columnAliasName = relationVariable + + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR + column.getColumnName(); + } + if (deletedColumn != null && StrUtil.equals(column.getColumnName(), deletedColumn.getColumnName())) { + continue; + } + if (this.castToInteger(column)) { + selectFieldBuider + .append("CAST(") + .append(table.getTableName()) + .append(".") + .append(column.getColumnName()) + .append(" AS ") + .append(intString) + .append(") \"") + .append(columnAliasName) + .append("\","); + } else if ("date".equals(column.getColumnType())) { + selectFieldBuider + .append("CAST(") + .append(table.getTableName()) + .append(".") + .append(column.getColumnName()) + .append(" AS CHAR(10)) \"") + .append(columnAliasName) + .append("\","); + } else { + selectFieldBuider + .append(table.getTableName()) + .append(".") + .append(column.getColumnName()) + .append(" \"") + .append(columnAliasName) + .append("\","); + } + } + return selectFieldBuider.substring(0, selectFieldBuider.length() - 1); + } + + private String makeSelectFieldsWithRelation( + OnlineTable masterTable, List relationList) { + String masterTableSelectFields = this.makeSelectFields(masterTable, null); + if (CollUtil.isEmpty(relationList)) { + return masterTableSelectFields; + } + StringBuilder selectFieldBuider = new StringBuilder(512); + selectFieldBuider.append(masterTableSelectFields).append(","); + for (OnlineDatasourceRelation relation : relationList) { + OnlineTable slaveTable = relation.getSlaveTable(); + String relationTableSelectFields = this.makeSelectFields(slaveTable, relation.getVariableName()); + selectFieldBuider.append(relationTableSelectFields).append(","); + } + return selectFieldBuider.substring(0, selectFieldBuider.length() - 1); + } + + private String makeDictSelectFields(OnlineDict onlineDict, boolean ignoreParentId) { + StringBuilder sb = new StringBuilder(128); + sb.append(onlineDict.getKeyColumnName()).append(" \"id\", "); + sb.append(onlineDict.getValueColumnName()).append(" \"name\""); + if (!ignoreParentId && BooleanUtil.isTrue(onlineDict.getTreeFlag())) { + sb.append(", ").append(onlineDict.getParentKeyColumnName()).append(" \"parentId\""); + } + return sb.toString(); + } + + private boolean castToInteger(OnlineColumn column) { + return "tinyint(1)".equals(column.getFullColumnType()); + } + + private String makeColumnNames(List columnDataList) { + StringBuilder sb = new StringBuilder(512); + for (ColumnData columnData : columnDataList) { + if (BooleanUtil.isTrue(columnData.getColumn().getAutoIncrement())) { + continue; + } + sb.append(columnData.getColumn().getColumnName()).append(","); + } + return sb.substring(0, sb.length() - 1); + } + + private void makeupColumnValue(ColumnData columnData) { + if (BooleanUtil.isTrue(columnData.getColumn().getAutoIncrement())) { + return; + } + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey())) { + if (columnData.getColumnValue() == null + && BooleanUtil.isFalse(columnData.getColumn().getAutoIncrement())) { + if (ObjectFieldType.LONG.equals(columnData.getColumn().getObjectFieldType())) { + columnData.setColumnValue(idGenerator.nextLongId()); + } else { + columnData.setColumnValue(idGenerator.nextStringId()); + } + } + } else if (columnData.getColumn().getFieldKind() != null) { + this.makeupColumnValueForFieldKind(columnData); + } else if (columnData.getColumn().getColumnDefault() != null + && columnData.getColumnValue() == null) { + Object v = onlineOperationHelper.convertToTypeValue( + columnData.getColumn(), columnData.getColumn().getColumnDefault()); + columnData.setColumnValue(v); + } + } + + private void makeupColumnValueForFieldKind(ColumnData columnData) { + switch (columnData.getColumn().getFieldKind()) { + case FieldKind.CREATE_TIME: + case FieldKind.UPDATE_TIME: + columnData.setColumnValue(LocalDateTime.now()); + break; + case FieldKind.CREATE_USER_ID: + case FieldKind.UPDATE_USER_ID: + columnData.setColumnValue(TokenData.takeFromRequest().getUserId()); + break; + case FieldKind.CREATE_DEPT_ID: + columnData.setColumnValue(TokenData.takeFromRequest().getDeptId()); + break; + case FieldKind.LOGIC_DELETE: + columnData.setColumnValue(GlobalDeletedFlag.NORMAL); + break; + default: + break; + } + } + + private List makeDefaultFilter(OnlineTable table, OnlineColumn column, String columnValue) { + List filterList = new LinkedList<>(); + OnlineFilterDto dataIdFilter = new OnlineFilterDto(); + dataIdFilter.setTableName(table.getTableName()); + dataIdFilter.setColumnName(column.getColumnName()); + dataIdFilter.setColumnValue(onlineOperationHelper.convertToTypeValue(column, columnValue)); + filterList.add(dataIdFilter); + if (table.getLogicDeleteColumn() != null) { + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(table.getTableName()); + filter.setColumnName(table.getLogicDeleteColumn().getColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + return filterList; + } + + private void doLogicDelete( + OnlineTable table, List filterList, String dataPermFilter) { + List updateColumnList = new LinkedList<>(); + ColumnData logicDeleteColumnData = new ColumnData(); + logicDeleteColumnData.setColumn(table.getLogicDeleteColumn()); + logicDeleteColumnData.setColumnValue(GlobalDeletedFlag.DELETED); + updateColumnList.add(logicDeleteColumnData); + this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + } + + private void doLogicDelete( + OnlineTable table, OnlineColumn filterColumn, String filterColumnValue, String dataPermFilter) { + List filterList = new LinkedList<>(); + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(table.getTableName()); + filter.setColumnName(filterColumn.getColumnName()); + filter.setColumnValue(onlineOperationHelper.convertToTypeValue(filterColumn, filterColumnValue)); + filterList.add(filter); + this.doLogicDelete(table, filterList, dataPermFilter); + } + + private void normalizeFilterList( + OnlineTable table, List oneToOneRelationList, List filterList) { + if (table.getLogicDeleteColumn() != null) { + if (filterList == null) { + filterList = new LinkedList<>(); + } + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(table.getTableName()); + filter.setColumnName(table.getLogicDeleteColumn().getColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + if (CollUtil.isEmpty(filterList)) { + return; + } + OnlineDblink dblink = onlineDblinkService.getById(table.getDblinkId()); + for (OnlineFilterDto filter : filterList) { + // oracle 日期字段的,后面要重写这段代码,以便具有更好的通用性。 + if (filter.getFilterType().equals(FieldFilterType.RANGE_FILTER)) { + this.makeRangeFilter(dblink, table, oneToOneRelationList, filter); + } + if (BooleanUtil.isTrue(filter.getDictMultiSelect())) { + filter.setFilterType(FieldFilterType.MULTI_LIKE); + List dictValueSet = StrUtil.split(filter.getColumnValue().toString(), ","); + filter.setColumnValueList( + dictValueSet.stream().map(v -> "%" + v + ",%").collect(Collectors.toSet())); + } + if (filter.getFilterType().equals(FieldFilterType.LIKE_FILTER)) { + filter.setColumnValue("%" + filter.getColumnValue() + "%"); + } else if (filter.getFilterType().equals(FieldFilterType.IN_LIST_FILTER) + && ObjectUtil.isNotEmpty(filter.getColumnValue())) { + filter.setColumnValueList( + new HashSet<>(StrUtil.split(filter.getColumnValue().toString(), ","))); + } + } + } + + private String normalizeSlaveTableAlias(List relationList, String s) { + if (CollUtil.isEmpty(relationList) || StrUtil.isBlank(s)) { + return s; + } + for (OnlineDatasourceRelation r : relationList) { + s = StrUtil.replace(s, r.getSlaveTable().getTableName() + ".", r.getVariableName() + "."); + } + return s; + } + + private void normalizeFiltersSlaveTableAlias( + List relationList, List filters) { + if (CollUtil.isEmpty(relationList) || CollUtil.isEmpty(filters)) { + return; + } + for (OnlineDatasourceRelation r : relationList) { + for (OnlineFilterDto filter : filters) { + if (StrUtil.equals(filter.getTableName(), r.getSlaveTable().getTableName())) { + filter.setTableName(r.getVariableName()); + } + } + } + } + + private void makeRangeFilter( + OnlineDblink dblink, + OnlineTable table, + List oneToOneRelationList, + OnlineFilterDto filter) { + if (!dblink.getDblinkType().equals(DblinkType.ORACLE)) { + return; + } + OnlineColumn column = table.getColumnMap().values().stream() + .filter(c -> c.getColumnName().equals(filter.getColumnName())).findFirst().orElse(null); + if (column == null && oneToOneRelationList != null) { + for (OnlineDatasourceRelation r : oneToOneRelationList) { + column = r.getSlaveTable().getColumnMap().values().stream() + .filter(c -> c.getColumnName().equals(filter.getColumnName())).findFirst().orElse(null); + if (column != null) { + break; + } + } + } + org.springframework.util.Assert.notNull(column, "column can't be NULL."); + filter.setIsOracleDate(StrUtil.equals(column.getObjectFieldType(), "Date")); + if (BooleanUtil.isTrue(filter.getIsOracleDate())) { + if (filter.getColumnValueStart() != null) { + filter.setColumnValueStart("TO_DATE('" + filter.getColumnValueStart() + "','YYYY-MM-DD HH24:MI:SS')"); + } + if (filter.getColumnValueEnd() != null) { + filter.setColumnValueEnd("TO_DATE('" + filter.getColumnValueEnd() + "','YYYY-MM-DD HH24:MI:SS')"); + } + } + } + + private String buildDataPermFilter(String tableName, String deptFilterColumnName, String userFilterColumnName) { + if (BooleanUtil.isFalse(dataFilterProperties.getEnabledDataPermFilter())) { + return null; + } + if (!GlobalThreadLocal.enabledDataFilter()) { + return null; + } + return processDataPerm(tableName, deptFilterColumnName, userFilterColumnName); + } + + private String buildDataPermFilter(OnlineTable table) { + if (BooleanUtil.isFalse(dataFilterProperties.getEnabledDataPermFilter())) { + return null; + } + if (!GlobalThreadLocal.enabledDataFilter()) { + return null; + } + String deptFilterColumnName = null; + String userFilterColumnName = null; + for (OnlineColumn column : table.getColumnMap().values()) { + if (BooleanUtil.isTrue(column.getDeptFilter())) { + deptFilterColumnName = column.getColumnName(); + } + if (BooleanUtil.isTrue(column.getUserFilter())) { + userFilterColumnName = column.getColumnName(); + } + } + return processDataPerm(table.getTableName(), deptFilterColumnName, userFilterColumnName); + } + + private String processDataPerm(String tableName, String deptFilterColumnName, String userFilterColumnName) { + TokenData tokenData = TokenData.takeFromRequest(); + if (Boolean.TRUE.equals(tokenData.getIsAdmin())) { + return null; + } + if (StrUtil.isAllBlank(deptFilterColumnName, userFilterColumnName)) { + return null; + } + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + Object cachedData = this.getCachedData(dataPermSessionKey); + if (cachedData == null) { + throw new NoDataPermException("No Related DataPerm found For OnlineForm Module."); + } + JSONObject allMenuDataPermMap = cachedData instanceof JSONObject + ? (JSONObject) cachedData : JSON.parseObject(cachedData.toString()); + JSONObject menuDataPermMap = this.getAndVerifyMenuDataPerm(allMenuDataPermMap, tableName); + Map dataPermMap = new HashMap<>(8); + for (Map.Entry entry : menuDataPermMap.entrySet()) { + dataPermMap.put(Integer.valueOf(entry.getKey()), entry.getValue().toString()); + } + if (MapUtil.isEmpty(dataPermMap)) { + throw new NoDataPermException(StrFormatter.format( + "No Related OnlineForm DataPerm found for table [{}].", tableName)); + } + if (dataPermMap.containsKey(DataPermRuleType.TYPE_ALL)) { + return null; + } + return doProcessDataPerm(tableName, deptFilterColumnName, userFilterColumnName, dataPermMap); + } + + private JSONObject getAndVerifyMenuDataPerm(JSONObject allMenuDataPermMap, String tableName) { + String menuId = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_MENU_ID); + if (menuId == null) { + menuId = ContextUtil.getHttpRequest().getParameter(ApplicationConstant.HTTP_HEADER_MENU_ID); + } + if (BooleanUtil.isFalse(dataFilterProperties.getEnableMenuPermVerify()) && menuId == null) { + menuId = ApplicationConstant.DATA_PERM_ALL_MENU_ID; + } + Assert.notNull(menuId); + JSONObject menuDataPermMap = allMenuDataPermMap.getJSONObject(menuId); + if (menuDataPermMap == null) { + menuDataPermMap = allMenuDataPermMap.getJSONObject(ApplicationConstant.DATA_PERM_ALL_MENU_ID); + } + if (menuDataPermMap == null) { + throw new NoDataPermException(StrFormatter.format( + "No Related OnlineForm DataPerm found for menuId [{}] and table [{}].", + menuId, tableName)); + } + if (BooleanUtil.isTrue(dataFilterProperties.getEnableMenuPermVerify())) { + String url = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_ORIGINAL_REQUEST_URL); + if (StrUtil.isBlank(url)) { + url = ContextUtil.getHttpRequest().getRequestURI(); + } + Assert.notNull(url); + if (!this.verifyMenuPerm(null, url, tableName) && !this.verifyMenuPerm(menuId, url, tableName)) { + String msg = StrFormatter.format("Mismatched OnlineForm DataPerm " + + "for menuId [{}] and url [{}] and SQL_ID [{}].", menuId, url, tableName); + throw new NoDataPermException(msg); + } + } + return menuDataPermMap; + } + + private Object getCachedData(String dataPermSessionKey) { + Object cachedData = null; + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.DATA_PERMISSION_CACHE.name()); + if (cache == null) { + return cachedData; + } + Cache.ValueWrapper wrapper = cache.get(dataPermSessionKey); + if (wrapper == null) { + cachedData = redissonClient.getBucket(dataPermSessionKey).get(); + if (cachedData != null) { + cache.put(dataPermSessionKey, JSON.parseObject(cachedData.toString())); + } + } else { + cachedData = wrapper.get(); + } + return cachedData; + } + + @SuppressWarnings("unchecked") + private boolean verifyMenuPerm(String menuId, String url, String tableName) { + String sessionId = TokenData.takeFromRequest().getSessionId(); + String menuPermSessionKey; + if (menuId != null) { + menuPermSessionKey = RedisKeyUtil.makeSessionMenuPermKey(sessionId, menuId); + } else { + menuPermSessionKey = RedisKeyUtil.makeSessionWhiteListPermKey(sessionId); + } + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.MENU_PERM_CACHE.name()); + if (cache == null) { + return false; + } + Cache.ValueWrapper wrapper = cache.get(menuPermSessionKey); + if (wrapper != null) { + Object cacheData = wrapper.get(); + if (cacheData != null) { + return ((Set) cacheData).contains(url); + } + } + RBucket bucket = redissonClient.getBucket(menuPermSessionKey); + if (!bucket.isExists()) { + String msg; + if (menuId == null) { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for WHITE_LIST and tableName [{}] with sessionId [{}].", tableName, sessionId); + } else { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for menuId [{}] and tableName[{}] with sessionId [{}].", menuId, tableName, sessionId); + } + throw new NoDataPermException(msg); + } + Set cachedMenuPermSet = new HashSet<>(JSONArray.parseArray(bucket.get(), String.class)); + cache.put(menuPermSessionKey, cachedMenuPermSet); + return cachedMenuPermSet.contains(url); + } + + private String doProcessDataPerm( + String tableName, String deptFilterColumnName, String userFilterColumnName, Map dataPermMap) { + List criteriaList = new LinkedList<>(); + for (Map.Entry entry : dataPermMap.entrySet()) { + String filterClause = processDataPermRule( + tableName, deptFilterColumnName, userFilterColumnName, entry.getKey(), entry.getValue()); + if (StrUtil.isNotBlank(filterClause)) { + criteriaList.add(filterClause); + } + } + if (CollUtil.isEmpty(criteriaList)) { + return null; + } + StringBuilder filterBuilder = new StringBuilder(128); + filterBuilder.append("("); + filterBuilder.append(CollUtil.join(criteriaList, " OR ")); + filterBuilder.append(")"); + return filterBuilder.toString(); + } + + private String processDataPermRule( + String tableName, String deptFilterColumnName, String userFilterColumnName, Integer ruleType, String dataIds) { + TokenData tokenData = TokenData.takeFromRequest(); + StringBuilder filter = new StringBuilder(128); + if (ruleType != DataPermRuleType.TYPE_USER_ONLY + && ruleType != DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS + && ruleType != DataPermRuleType.TYPE_DEPT_USERS) { + return this.processDeptDataPermRule(tableName, deptFilterColumnName, ruleType, dataIds); + } + if (StrUtil.isBlank(userFilterColumnName)) { + log.warn("No UserFilterColumn for ONLINE table [{}] but USER_FILTER_DATA_PERM exists", tableName); + return filter.toString(); + } + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + if (ruleType == DataPermRuleType.TYPE_USER_ONLY) { + filter.append(userFilterColumnName).append(" = ").append(tokenData.getUserId()); + } else { + filter.append(userFilterColumnName) + .append(" IN (") + .append(dataIds) + .append(") "); + } + return filter.toString(); + } + + private String processDeptDataPermRule( + String tableName, String deptFilterColumnName, Integer ruleType, String deptIds) { + TokenData tokenData = TokenData.takeFromRequest(); + StringBuilder filter = new StringBuilder(256); + if (StrUtil.isBlank(deptFilterColumnName)) { + log.warn("No DeptFilterColumn for ONLINE table [{}] but DEPT_FILTER_DATA_PERM exists", tableName); + return filter.toString(); + } + if (ruleType == DataPermRuleType.TYPE_DEPT_ONLY) { + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName).append(" = ").append(tokenData.getDeptId()); + } else if (ruleType == DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id = ") + .append(tokenData.getDeptId()) + .append(AND); + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName) + .append(" = ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id IN (") + .append(deptIds) + .append(") AND "); + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName) + .append(" = ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) { + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName).append(" IN (").append(deptIds).append(") "); + } + return filter.toString(); + } + + @Data + private static class VirtualColumnWhereClause { + private Long tableId; + private Long columnId; + private Integer operatorType; + private Object value; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java new file mode 100644 index 00000000..4b3ddaab --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java @@ -0,0 +1,295 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlinePageDatasourceMapper; +import com.orangeforms.common.online.dao.OnlinePageMapper; +import com.orangeforms.common.online.model.OnlinePage; +import com.orangeforms.common.online.model.OnlinePageDatasource; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineFormService; +import com.orangeforms.common.online.service.OnlinePageService; +import com.github.pagehelper.Page; +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.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * 在线表单页面数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlinePageService") +public class OnlinePageServiceImpl extends BaseService implements OnlinePageService { + + @Autowired + private OnlinePageMapper onlinePageMapper; + @Autowired + private OnlinePageDatasourceMapper onlinePageDatasourceMapper; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlinePageMapper; + } + + /** + * 保存新增对象。 + * + * @param onlinePage 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlinePage saveNew(OnlinePage onlinePage) { + TokenData tokenData = TokenData.takeFromRequest(); + onlinePage.setPageId(idGenerator.nextLongId()); + onlinePage.setAppCode(tokenData.getAppCode()); + onlinePage.setTenantId(tokenData.getTenantId()); + Date now = new Date(); + onlinePage.setUpdateTime(now); + onlinePage.setCreateTime(now); + onlinePage.setCreateUserId(tokenData.getUserId()); + onlinePage.setUpdateUserId(tokenData.getUserId()); + onlinePage.setPublished(false); + MyModelUtil.setDefaultValue(onlinePage, "status", PageStatus.BASIC); + onlinePageMapper.insert(onlinePage); + return onlinePage; + } + + /** + * 更新数据对象。 + * + * @param onlinePage 更新的对象。 + * @param originalOnlinePage 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlinePage onlinePage, OnlinePage originalOnlinePage) { + TokenData tokenData = TokenData.takeFromRequest(); + onlinePage.setAppCode(tokenData.getAppCode()); + onlinePage.setTenantId(tokenData.getTenantId()); + onlinePage.setUpdateTime(new Date()); + onlinePage.setUpdateUserId(tokenData.getUserId()); + onlinePage.setCreateTime(originalOnlinePage.getCreateTime()); + onlinePage.setCreateUserId(originalOnlinePage.getCreateUserId()); + onlinePage.setPublished(originalOnlinePage.getPublished()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + return onlinePageMapper.update(onlinePage, false) == 1; + } + + /** + * 更新页面对象的发布状态。 + * + * @param pageId 页面对象Id。 + * @param published 新的状态。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void updatePublished(Long pageId, Boolean published) { + OnlinePage onlinePage = new OnlinePage(); + onlinePage.setPageId(pageId); + onlinePage.setPublished(published); + onlinePage.setUpdateTime(new Date()); + onlinePage.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + onlinePageMapper.update(onlinePage); + } + + /** + * 删除指定数据,及其包含的表单和数据源等。 + * + * @param pageId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long pageId) { + if (onlinePageMapper.deleteById(pageId) == 0) { + return false; + } + // 开始删除关联表单。 + onlineFormService.removeByPageId(pageId); + // 先获取出关联的表单和数据源。 + OnlinePageDatasource pageDatasourceFilter = new OnlinePageDatasource(); + pageDatasourceFilter.setPageId(pageId); + List pageDatasourceList = + onlinePageDatasourceMapper.selectListByQuery(QueryWrapper.create(pageDatasourceFilter)); + if (CollUtil.isNotEmpty(pageDatasourceList)) { + for (OnlinePageDatasource pageDatasource : pageDatasourceList) { + onlineDatasourceService.remove(pageDatasource.getDatasourceId()); + } + } + return true; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlinePageListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlinePageList(OnlinePage filter, String orderBy) { + if (filter == null) { + filter = new OnlinePage(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlinePageMapper.getOnlinePageList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlinePageList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlinePageListWithRelation(OnlinePage filter, String orderBy) { + List resultList = this.getOnlinePageList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 批量添加多对多关联关系。 + * + * @param onlinePageDatasourceList 多对多关联表对象集合。 + * @param pageId 主表Id。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void addOnlinePageDatasourceList(List onlinePageDatasourceList, Long pageId) { + for (OnlinePageDatasource onlinePageDatasource : onlinePageDatasourceList) { + onlinePageDatasource.setPageId(pageId); + onlinePageDatasourceMapper.insert(onlinePageDatasource); + } + } + + /** + * 获取中间表数据。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 中间表对象。 + */ + @Override + public OnlinePageDatasource getOnlinePageDatasource(Long pageId, Long datasourceId) { + OnlinePageDatasource filter = new OnlinePageDatasource(); + filter.setPageId(pageId); + filter.setDatasourceId(datasourceId); + return onlinePageDatasourceMapper.selectOneByQuery(QueryWrapper.create(filter)); + } + + @Override + public List getOnlinePageDatasourceListByPageId(Long pageId) { + return onlinePageDatasourceMapper.selectListByQuery( + new QueryWrapper().eq(OnlinePageDatasource::getPageId, pageId)); + } + + /** + * 根据数据源Id,返回使用该数据源的OnlinePage对象。 + * + * @param datasourceId 数据源Id。 + * @return 使用该数据源的页面列表。 + */ + @Override + public List getOnlinePageListByDatasourceId(Long datasourceId) { + OnlinePage filter = new OnlinePage(); + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlinePageMapper.getOnlinePageListByDatasourceId(datasourceId, filter); + } + + /** + * 移除单条多对多关系。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeOnlinePageDatasource(Long pageId, Long datasourceId) { + OnlinePageDatasource filter = new OnlinePageDatasource(); + filter.setPageId(pageId); + filter.setDatasourceId(datasourceId); + return onlinePageDatasourceMapper.deleteByQuery(QueryWrapper.create(filter)) > 0; + } + + @Override + public boolean existByPageCode(String pageCode) { + OnlinePage filter = new OnlinePage(); + filter.setPageCode(pageCode); + return CollUtil.isNotEmpty(this.getOnlinePageList(filter, null)); + } + + @Override + public List getNotInListWithNonTenant(List pageIds, String orderBy) { + QueryWrapper queryWrapper = new QueryWrapper(); + if (CollUtil.isNotEmpty(pageIds)) { + queryWrapper.notIn(OnlinePage::getPageId, pageIds); + } + queryWrapper.isNull(OnlinePage::getTenantId); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return onlinePageMapper.selectListByQuery(queryWrapper); + } + + @Override + public List getInListWithNonTenant(List pageIds, String orderBy) { + if (CollUtil.isEmpty(pageIds)) { + return new LinkedList<>(); + } + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(OnlinePage::getPageId, pageIds); + queryWrapper.isNull(OnlinePage::getTenantId); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.orderBy(orderBy); + } + return onlinePageMapper.selectListByQuery(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java new file mode 100644 index 00000000..921e7004 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java @@ -0,0 +1,245 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineColumnRuleMapper; +import com.orangeforms.common.online.dao.OnlineRuleMapper; +import com.orangeforms.common.online.model.OnlineColumnRule; +import com.orangeforms.common.online.model.OnlineRule; +import com.orangeforms.common.online.service.OnlineRuleService; +import com.github.pagehelper.Page; +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.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 验证规则数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineRuleService") +public class OnlineRuleServiceImpl extends BaseService implements OnlineRuleService { + + @Autowired + private OnlineRuleMapper onlineRuleMapper; + @Autowired + private OnlineColumnRuleMapper onlineColumnRuleMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private RedissonClient redissonClient; + + /** + * 所有字段规则使用同一个键。 + */ + private static final String ONLINE_RULE_CACHE_KEY = "ONLINE_RULE"; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineRuleMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineRule 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineRule saveNew(OnlineRule onlineRule) { + commonRedisUtil.evictFormCache(ONLINE_RULE_CACHE_KEY); + TokenData tokenData = TokenData.takeFromRequest(); + onlineRule.setRuleId(idGenerator.nextLongId()); + onlineRule.setAppCode(tokenData.getAppCode()); + Date now = new Date(); + onlineRule.setUpdateTime(now); + onlineRule.setCreateTime(now); + onlineRule.setCreateUserId(tokenData.getUserId()); + onlineRule.setUpdateUserId(tokenData.getUserId()); + onlineRule.setBuiltin(false); + onlineRule.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.setDefaultValue(onlineRule, "pattern", ""); + onlineRuleMapper.insert(onlineRule); + return onlineRule; + } + + /** + * 更新数据对象。 + * + * @param onlineRule 更新的对象。 + * @param originalOnlineRule 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineRule onlineRule, OnlineRule originalOnlineRule) { + commonRedisUtil.evictFormCache(ONLINE_RULE_CACHE_KEY); + TokenData tokenData = TokenData.takeFromRequest(); + onlineRule.setAppCode(tokenData.getAppCode()); + onlineRule.setUpdateTime(new Date()); + onlineRule.setUpdateUserId(tokenData.getUserId()); + onlineRule.setCreateTime(originalOnlineRule.getCreateTime()); + onlineRule.setCreateUserId(originalOnlineRule.getCreateUserId()); + return onlineRuleMapper.update(onlineRule, false) == 1; + } + + /** + * 删除指定数据。 + * + * @param ruleId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long ruleId) { + commonRedisUtil.evictFormCache(ONLINE_RULE_CACHE_KEY); + if (onlineRuleMapper.deleteById(ruleId) == 0) { + return false; + } + // 开始删除多对多父表的关联 + OnlineColumnRule onlineColumnRule = new OnlineColumnRule(); + onlineColumnRule.setRuleId(ruleId); + onlineColumnRuleMapper.deleteByQuery(QueryWrapper.create(onlineColumnRule)); + return true; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineRuleListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineRuleList(OnlineRule filter, String orderBy) { + if (filter == null) { + filter = new OnlineRule(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineRuleMapper.getOnlineRuleList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineRuleList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineRuleListWithRelation(OnlineRule filter, String orderBy) { + List resultList = this.getOnlineRuleList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getNotInOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy) { + if (filter == null) { + filter = new OnlineRule(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + List resultList = + onlineRuleMapper.getNotInOnlineRuleListByColumnId(columnId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy) { + List resultList = + onlineRuleMapper.getOnlineRuleListByColumnId(columnId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + /** + * 返回指定字段Id列表关联的字段规则对象列表。 + * + * @param columnIdSet 指定的字段Id列表。 + * @return 关联的字段规则对象列表。 + */ + @Override + public List getOnlineColumnRuleListByColumnIds(Set columnIdSet) { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.in(OnlineColumnRule::getColumnId, columnIdSet); + List columnRuleList = onlineColumnRuleMapper.selectListByQuery(queryWrapper); + if (CollUtil.isEmpty(columnRuleList)) { + return columnRuleList; + } + List ruleList; + RBucket bucket = redissonClient.getBucket(ONLINE_RULE_CACHE_KEY); + if (bucket.isExists()) { + ruleList = JSONArray.parseArray(bucket.get(), OnlineRule.class); + } else { + ruleList = this.getAllList(); + if (CollUtil.isNotEmpty(ruleList)) { + bucket.set(JSONArray.toJSONString(ruleList)); + } + } + if (CollUtil.isEmpty(ruleList)) { + return columnRuleList; + } + Map ruleMap = ruleList.stream().collect(Collectors.toMap(OnlineRule::getRuleId, c -> c)); + for (OnlineColumnRule columnRule : columnRuleList) { + columnRule.setOnlineRule(ruleMap.get(columnRule.getRuleId())); + } + return columnRuleList; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java new file mode 100644 index 00000000..06ee87b0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java @@ -0,0 +1,194 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineTableMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.google.common.base.CaseFormat; +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.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 数据表数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineTableService") +public class OnlineTableServiceImpl extends BaseService implements OnlineTableService { + + @Autowired + private OnlineTableMapper onlineTableMapper; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + + /** + * 在线对象表的缺省缓存时间(小时)。 + */ + private static final int DEFAULT_CACHED_TABLE_HOURS = 168; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineTableMapper; + } + + /** + * 基于数据库表保存新增对象。 + * + * @param sqlTable 数据库表对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineTable saveNewFromSqlTable(SqlTable sqlTable) { + OnlineTable onlineTable = new OnlineTable(); + TokenData tokenData = TokenData.takeFromRequest(); + onlineTable.setAppCode(tokenData.getAppCode()); + onlineTable.setDblinkId(sqlTable.getDblinkId()); + onlineTable.setTableId(idGenerator.nextLongId()); + onlineTable.setTableName(sqlTable.getTableName()); + String modelName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, sqlTable.getTableName()); + onlineTable.setModelName(modelName); + Date now = new Date(); + onlineTable.setUpdateTime(now); + onlineTable.setCreateTime(now); + onlineTable.setCreateUserId(tokenData.getUserId()); + onlineTable.setUpdateUserId(tokenData.getUserId()); + onlineTableMapper.insert(onlineTable); + List columnList = onlineColumnService.saveNewList(sqlTable.getColumnList(), onlineTable.getTableId()); + onlineTable.setColumnList(columnList); + return onlineTable; + } + + /** + * 删除指定表及其关联的字段数据。 + * + * @param tableId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long tableId) { + if (onlineTableMapper.deleteById(tableId) == 0) { + return false; + } + this.evictTableCache(tableId); + onlineColumnService.removeByTableId(tableId); + return true; + } + + /** + * 删除指定数据表Id集合中的表,及其关联字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByTableIdSet(Set tableIdSet) { + tableIdSet.forEach(this::evictTableCache); + onlineTableMapper.deleteByQuery(new QueryWrapper().in(OnlineTable::getTableId, tableIdSet)); + onlineColumnService.removeByTableIdSet(tableIdSet); + } + + /** + * 根据数据源Id,获取该数据源及其关联所引用的数据表列表。 + * + * @param datasourceId 指定的数据源Id。 + * @return 该数据源及其关联所引用的数据表列表。 + */ + @Override + public List getOnlineTableListByDatasourceId(Long datasourceId) { + return onlineTableMapper.getOnlineTableListByDatasourceId(datasourceId); + } + + /** + * 从缓存中获取指定的表数据及其关联字段列表。优先从缓存中读取,如果不存在则从数据库中读取,并同步到缓存。 + * 该接口方法仅仅用户在线表单的动态数据操作接口,而非在线表单的配置接口。 + * + * @param tableId 表主键Id。 + * @return 查询后的在线表对象。 + */ + @Override + public OnlineTable getOnlineTableFromCache(Long tableId) { + String redisKey = OnlineRedisKeyUtil.makeOnlineTableKey(tableId); + RBucket tableBucket = redissonClient.getBucket(redisKey); + if (tableBucket.isExists()) { + String tableInfo = tableBucket.get(); + return JSON.parseObject(tableInfo, OnlineTable.class); + } + OnlineTable table = this.getByIdWithRelation(tableId, MyRelationParam.full()); + if (table == null) { + return null; + } + for (OnlineColumn column : table.getColumnList()) { + if (BooleanUtil.isTrue(column.getPrimaryKey())) { + table.setPrimaryKeyColumn(column); + continue; + } + if (ObjectUtil.equal(column.getFieldKind(), FieldKind.LOGIC_DELETE)) { + table.setLogicDeleteColumn(column); + } + } + Map columnMap = + table.getColumnList().stream().collect(Collectors.toMap(OnlineColumn::getColumnId, c -> c)); + table.setColumnMap(columnMap); + table.setColumnList(null); + tableBucket.set(JSON.toJSONString(table)); + tableBucket.expire(DEFAULT_CACHED_TABLE_HOURS, TimeUnit.HOURS); + return table; + } + + @Override + public OnlineColumn getOnlineColumnFromCache(Long tableId, Long columnId) { + OnlineTable table = this.getOnlineTableFromCache(tableId); + if (table == null) { + return null; + } + return table.getColumnMap().get(columnId); + } + + private void evictTableCache(Long tableId) { + String tableIdKey = OnlineRedisKeyUtil.makeOnlineTableKey(tableId); + redissonClient.getBucket(tableIdKey).delete(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java new file mode 100644 index 00000000..c133ccee --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java @@ -0,0 +1,176 @@ +package com.orangeforms.common.online.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.online.dao.OnlineVirtualColumnMapper; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineVirtualColumn; +import com.orangeforms.common.online.model.constant.VirtualType; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineVirtualColumnService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 虚拟字段数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineVirtualColumnService") +public class OnlineVirtualColumnServiceImpl + extends BaseService implements OnlineVirtualColumnService { + + @Autowired + private OnlineVirtualColumnMapper onlineVirtualColumnMapper; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineVirtualColumnMapper; + } + + /** + * 保存新增对象。 + * + * @param virtualColumn 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineVirtualColumn saveNew(OnlineVirtualColumn virtualColumn) { + virtualColumn.setVirtualColumnId(idGenerator.nextLongId()); + if (virtualColumn.getVirtualType().equals(VirtualType.AGGREGATION)) { + OnlineDatasource datasource = onlineDatasourceService.getById(virtualColumn.getDatasourceId()); + virtualColumn.setTableId(datasource.getMasterTableId()); + } + onlineVirtualColumnMapper.insert(virtualColumn); + return virtualColumn; + } + + /** + * 更新数据对象。 + * + * @param virtualColumn 更新的对象。 + * @param originalVirtualColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineVirtualColumn virtualColumn, OnlineVirtualColumn originalVirtualColumn) { + if (virtualColumn.getVirtualType().equals(VirtualType.AGGREGATION) + && !virtualColumn.getDatasourceId().equals(originalVirtualColumn.getDatasourceId())) { + OnlineDatasource datasource = onlineDatasourceService.getById(virtualColumn.getDatasourceId()); + virtualColumn.setTableId(datasource.getMasterTableId()); + } + return onlineVirtualColumnMapper.update(virtualColumn, false) == 1; + } + + /** + * 删除指定数据。 + * + * @param virtualColumnId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long virtualColumnId) { + return onlineVirtualColumnMapper.deleteById(virtualColumnId) == 1; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineVirtualColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineVirtualColumnList(OnlineVirtualColumn filter, String orderBy) { + return onlineVirtualColumnMapper.getOnlineVirtualColumnList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineVirtualColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineVirtualColumnListWithRelation(OnlineVirtualColumn filter, String orderBy) { + List resultList = onlineVirtualColumnMapper.getOnlineVirtualColumnList(filter, orderBy); + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 根据数据表的集合,查询关联的虚拟字段数据列表。 + * @param tableIdSet 在线数据表Id集合。 + * @return 关联的虚拟字段数据列表。 + */ + @Override + public List getOnlineVirtualColumnListByTableIds(Set tableIdSet) { + return onlineVirtualColumnMapper.selectListByQuery( + new QueryWrapper().in(OnlineVirtualColumn::getTableId, tableIdSet)); + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param virtualColumn 最新数据对象。 + * @param originalVirtualColumn 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData(OnlineVirtualColumn virtualColumn, OnlineVirtualColumn originalVirtualColumn) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + if (this.needToVerify(virtualColumn, originalVirtualColumn, OnlineVirtualColumn::getDatasourceId) + && !onlineDatasourceService.existId(virtualColumn.getDatasourceId())) { + return CallResult.error(String.format(errorMessageFormat, "数据源Id")); + } + if (this.needToVerify(virtualColumn, originalVirtualColumn, OnlineVirtualColumn::getRelationId) + && !onlineDatasourceRelationService.existId(virtualColumn.getRelationId())) { + return CallResult.error(String.format(errorMessageFormat, "数据源关联Id")); + } + if (this.needToVerify(virtualColumn, originalVirtualColumn, OnlineVirtualColumn::getAggregationColumnId) + && !onlineColumnService.existId(virtualColumn.getAggregationColumnId())) { + return CallResult.error(String.format(errorMessageFormat, "聚合字段Id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java new file mode 100644 index 00000000..f40866dc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单使用的常量数据。。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineConstant { + + /** + * 数据源关联变量名和从表字段名之间的连接字符串。 + */ + public static final String RELATION_TABLE_COLUMN_SEPARATOR = "__"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private OnlineConstant() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java new file mode 100644 index 00000000..a46868b3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.util; + +import org.springframework.stereotype.Component; + +/** + * 在线表单自定义扩展工厂类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class OnlineCustomExtFactory { + + private OnlineCustomMaskFieldHandler customMaskFieldHandler = new OnlineCustomMaskFieldHandler(); + + /** + * 设置自定义脱敏规则处理器对象。推荐设置的对象为Bean对象,并在服务启动过程中完成自动注册,运行时直接使用即可。 + * + * @param customMaskFieldHandler 自定义脱敏规则处理器对象。 + */ + public void setCustomMaskFieldHandler(OnlineCustomMaskFieldHandler customMaskFieldHandler) { + this.customMaskFieldHandler = customMaskFieldHandler; + } + + /** + * 返回在线表单的自定义脱敏规则处理器对象。该Bean对象需要在业务代码中实现自行实现。 + * + * @return 在线表单的自定义脱敏规则处理器对象。 + */ + public OnlineCustomMaskFieldHandler getCustomMaskFieldHandler() { + return customMaskFieldHandler; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java new file mode 100644 index 00000000..e99b0e58 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单自定义脱敏处理器的默认实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineCustomMaskFieldHandler { + + /** + * 处理自定义的脱敏数据。可以根据表名和字段名,使用不同的自定义脱敏规则。 + * + * @param appCode 应用编码。如果不是第三方接入的应用,该值可能为null。 + * @param tableName 在线表单对应的表名。 + * @param columnName 在线表单对应的表字段名 + * @param data 待脱敏的数据。 + * @param maskChar 脱敏掩码字符。 + * @return 脱敏后的数据。 + */ + public String handleMask(String appCode, String tableName, String columnName, String data, char maskChar) { + throw new UnsupportedOperationException( + "在运行时抛出该异常,主要为了及时提醒用户提供自己的处理器实现类。请在业务工程中提供该类的具体实现类!"); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java new file mode 100644 index 00000000..a4b765a9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.online.util; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.dbutil.util.DataSourceUtil; +import com.orangeforms.common.online.model.OnlineDblink; +import com.orangeforms.common.online.service.OnlineDblinkService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 在线表单模块动态加载的数据源工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class OnlineDataSourceUtil extends DataSourceUtil { + + @Autowired + private OnlineDblinkService dblinkService; + + @Override + protected int getDblinkTypeByDblinkId(Long dblinkId) { + DataSourceProvider provider = this.dblinkProviderMap.get(dblinkId); + if (provider != null) { + return provider.getDblinkType(); + } + OnlineDblink dblink = dblinkService.getById(dblinkId); + if (dblink == null) { + throw new MyRuntimeException("Online DblinkId [" + dblinkId + "] doesn't exist!"); + } + this.dblinkProviderMap.put(dblinkId, this.getProvider(dblink.getDblinkType())); + return dblink.getDblinkType(); + } + + @Override + protected String getDblinkConfigurationByDblinkId(Long dblinkId) { + OnlineDblink dblink = dblinkService.getById(dblinkId); + if (dblink == null) { + throw new MyRuntimeException("Online DblinkId [" + dblinkId + "] doesn't exist!"); + } + return dblink.getConfiguration(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java new file mode 100644 index 00000000..4fe6a307 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java @@ -0,0 +1,419 @@ +package com.orangeforms.common.online.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.object.ColumnData; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线表单操作的通用帮助对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class OnlineOperationHelper { + + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private SessionCacheHelper cacheHelper; + + /** + * 验证并获取数据源数据。 + * + * @param datasourceId 数据源Id。 + * @return 数据源详情数据。 + */ + public ResponseResult verifyAndGetDatasource(Long datasourceId) { + String errorMessage; + OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceFromCache(datasourceId); + if (datasource == null) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!StrUtil.equals(datasource.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该数据源Id"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineTable masterTable = onlineTableService.getOnlineTableFromCache(datasource.getMasterTableId()); + if (masterTable == null) { + errorMessage = "数据验证失败,数据源主表Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + datasource.setMasterTable(masterTable); + return ResponseResult.success(datasource); + } + + /** + * 验证并获取数据源的关联数据。 + * + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @return 数据源的关联详情数据。 + */ + public ResponseResult verifyAndGetRelation(Long datasourceId, Long relationId) { + String errorMessage; + OnlineDatasourceRelation relation = + onlineDatasourceRelationService.getOnlineDatasourceRelationFromCache(datasourceId, relationId); + if (relation == null || !relation.getDatasourceId().equals(datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(relation.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该数据源关联Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineTable slaveTable = onlineTableService.getOnlineTableFromCache(relation.getSlaveTableId()); + if (slaveTable == null) { + errorMessage = "数据验证失败,数据源关联 [" + relation.getRelationName() + " ] 引用的从表不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + relation.setSlaveTable(slaveTable); + relation.setSlaveColumn(slaveTable.getColumnMap().get(relation.getSlaveColumnId())); + return ResponseResult.success(relation); + } + + /** + * 验证并获取数据源的指定类型关联数据。 + * + * @param datasourceId 数据源Id。 + * @param relationType 数据源关联类型。 + * @return 数据源指定关联类型的关联数据详情列表。 + */ + public ResponseResult> verifyAndGetRelationList( + Long datasourceId, Integer relationType) { + String errorMessage; + List relationList = onlineDatasourceRelationService + .getOnlineDatasourceRelationListFromCache(CollUtil.newHashSet(datasourceId)); + if (relationType != null) { + relationList = relationList.stream() + .filter(r -> r.getRelationType().equals(relationType)).collect(Collectors.toList()); + } + for (OnlineDatasourceRelation relation : relationList) { + OnlineTable slaveTable = onlineTableService.getOnlineTableFromCache(relation.getSlaveTableId()); + if (slaveTable == null) { + errorMessage = "数据验证失败,数据源关联 [" + relation.getRelationName() + "] 的从表Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + relation.setSlaveTable(slaveTable); + } + return ResponseResult.success(relationList); + } + + /** + * 构建在线表的数据记录。 + * + * @param table 在线数据表对象。 + * @param tableData 在线数据表数据。 + * @param forUpdate 是否为更新。 + * @param ignoreSetColumnId 忽略设置的字段Id。 + * @return 在线表的数据记录。 + */ + public ResponseResult> buildTableData( + OnlineTable table, JSONObject tableData, boolean forUpdate, Long ignoreSetColumnId) { + List columnDataList = new LinkedList<>(); + String errorMessage; + for (OnlineColumn column : table.getColumnMap().values()) { + // 判断一下是否为需要自动填入的字段,如果是,这里就都暂时给空值了,后续操作会自动填补。 + // 这里还能避免一次基于tableData的查询,能快几纳秒也是好的。 + if (this.isAutoSettingField(column) || ObjectUtil.equal(column.getColumnId(), ignoreSetColumnId)) { + columnDataList.add(new ColumnData(column, null)); + continue; + } + Object value = this.getColumnValue(tableData, column); + // 对于主键数据的处理。 + if (BooleanUtil.isTrue(column.getPrimaryKey())) { + // 如果是更新则必须包含主键参数。 + if (forUpdate && value == null) { + errorMessage = "数据验证失败,数据表 [" + + table.getTableName() + "] 主键字段 [" + column.getColumnName() + "] 不能为空值!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } else { + if (value == null && !column.getNullable() && StrUtil.isBlank(column.getEncodedRule())) { + errorMessage = "数据验证失败,数据表 [" + + table.getTableName() + "] 字段 [" + column.getColumnName() + "] 不能为空值!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + columnDataList.add(new ColumnData(column, value)); + } + return ResponseResult.success(columnDataList); + } + + /** + * 构建多个一对多从表的数据列表。 + * + * @param datasourceId 数据源Id。 + * @param slaveData 多个一对多从表数据的JSON对象。 + * @return 构建后的多个一对多从表数据列表。 + */ + public ResponseResult>> buildSlaveDataList( + Long datasourceId, JSONObject slaveData) { + if (slaveData == null) { + return ResponseResult.success(null); + } + Map> relationDataMap = new HashMap<>(slaveData.size()); + for (String key : slaveData.keySet()) { + Long relationId = Long.parseLong(key); + ResponseResult relationResult = this.verifyAndGetRelation(datasourceId, relationId); + if (!relationResult.isSuccess()) { + return ResponseResult.errorFrom(relationResult); + } + OnlineDatasourceRelation relation = relationResult.getData(); + List relationDataList = new LinkedList<>(); + relationDataMap.put(relation, relationDataList); + if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + JSONArray slaveObjectArray = slaveData.getJSONArray(key); + for (int i = 0; i < slaveObjectArray.size(); i++) { + relationDataList.add(slaveObjectArray.getJSONObject(i)); + } + } else if (relation.getRelationType().equals(RelationType.ONE_TO_ONE)) { + JSONObject o = slaveData.getJSONObject(key); + if (MapUtil.isNotEmpty(o)) { + relationDataList.add(o); + } + } + } + return ResponseResult.success(relationDataMap); + } + + /** + * 将字符型字段值转换为与参数字段类型匹配的字段值。 + * + * @param column 在线表单字段。 + * @param dataId 字符型字段值。 + * @return 转换后与参数字段类型匹配的字段值。 + */ + public Serializable convertToTypeValue(OnlineColumn column, String dataId) { + if (dataId == null) { + return null; + } + if (column == null) { + return dataId; + } + if ("Long".equals(column.getObjectFieldType())) { + return Long.valueOf(dataId); + } else if ("Integer".equals(column.getObjectFieldType())) { + return Integer.valueOf(dataId); + } + return dataId; + } + + /** + * 将字符型字段值集合转换为与参数字段类型匹配的字段值集合。 + * + * @param column 在线表单字段。 + * @param dataIdSet 字符型字段值集合。 + * @return 转换后与参数字段类型匹配的字段值集合。 + */ + public Set convertToTypeValue(OnlineColumn column, Set dataIdSet) { + Set resultSet = new HashSet<>(); + if (dataIdSet == null) { + return resultSet; + } + if ("Long".equals(column.getObjectFieldType())) { + return dataIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + } else if ("Integer".equals(column.getObjectFieldType())) { + return dataIdSet.stream().map(Integer::valueOf).collect(Collectors.toSet()); + } else { + resultSet.addAll(dataIdSet); + } + return resultSet; + } + + /** + * 下载数据。 + * + * @param table 在线表对象。 + * @param dataId 在线表数据主键Id。 + * @param fieldName 数据表字段名。 + * @param filename 下载文件名。 + * @param asImage 是否为图片。 + * @param response HTTP 应对对象。 + */ + public void doDownload( + OnlineTable table, String dataId, String fieldName, String filename, Boolean asImage, HttpServletResponse response) { + // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。 + // 否则有可能给前端返回的是200的错误码。 + try { + // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。 + if (ObjectUtil.isEmpty(dataId)) { + if (!cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } else { + Map dataMap = + onlineOperationService.getMasterData(table, null, null, dataId); + if (dataMap == null) { + ResponseResult.output(HttpServletResponse.SC_NOT_FOUND); + return; + } + String fieldJsonData = (String) dataMap.get(fieldName); + if (!this.canDownload(fieldJsonData, filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + ResponseResult verifyResult = this.doVerifyUpDownloadFileColumn(table, fieldName, asImage); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, verifyResult); + return; + } + OnlineColumn downloadColumn = verifyResult.getData(); + if (downloadColumn.getUploadFileSystemType() == null) { + downloadColumn.setUploadFileSystemType(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal()); + } + if (!downloadColumn.getUploadFileSystemType().equals(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal())) { + downloadColumn.setUploadFileSystemType(onlineProperties.getDistributeStoreType()); + } + UploadStoreTypeEnum uploadStoreType = + UploadStoreTypeEnum.values()[downloadColumn.getUploadFileSystemType()]; + BaseUpDownloader upDownloader = upDownloaderFactory.get(uploadStoreType); + upDownloader.doDownload(onlineProperties.getUploadFileBaseDir(), + table.getModelName(), fieldName, filename, asImage, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + /** + * 上传数据。 + * + * @param table 在线表对象。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片。 + * @param uploadFile 上传的文件。 + */ + public void doUpload(OnlineTable table, String fieldName, Boolean asImage, MultipartFile uploadFile) + throws IOException { + ResponseResult verifyResult = this.doVerifyUpDownloadFileColumn(table, fieldName, asImage); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, verifyResult); + return; + } + OnlineColumn uploadColumn = verifyResult.getData(); + if (uploadColumn.getUploadFileSystemType() == null) { + uploadColumn.setUploadFileSystemType(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal()); + } + if (!uploadColumn.getUploadFileSystemType().equals(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal())) { + uploadColumn.setUploadFileSystemType(onlineProperties.getDistributeStoreType()); + } + UploadStoreTypeEnum uploadStoreType = UploadStoreTypeEnum.values()[uploadColumn.getUploadFileSystemType()]; + BaseUpDownloader upDownloader = upDownloaderFactory.get(uploadStoreType); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + onlineProperties.getUploadFileBaseDir(), table.getModelName(), fieldName, asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + // 动态表单的下载url和普通表单有所不同,由前端负责动态拼接。 + responseInfo.setDownloadUri(null); + cacheHelper.putSessionUploadFile(responseInfo.getFilename()); + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + private ResponseResult doVerifyUpDownloadFileColumn( + OnlineTable table, String fieldName, Boolean asImage) { + OnlineColumn column = this.getOnlineColumnByName(table, fieldName); + if (column == null) { + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD); + } + if (BooleanUtil.isTrue(asImage)) { + if (ObjectUtil.notEqual(column.getFieldKind(), FieldKind.UPLOAD_IMAGE)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD); + } + } else { + if (ObjectUtil.notEqual(column.getFieldKind(), FieldKind.UPLOAD)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD); + } + } + return ResponseResult.success(column); + } + + private OnlineColumn getOnlineColumnByName(OnlineTable table, String fieldName) { + for (OnlineColumn column : table.getColumnMap().values()) { + if (column.getColumnName().equals(fieldName)) { + return column; + } + } + return null; + } + + private Object getColumnValue(JSONObject tableData, OnlineColumn column) { + Object value = tableData.get(column.getColumnName()); + if (value != null) { + if (ObjectFieldType.LONG.equals(column.getObjectFieldType())) { + value = Long.valueOf(value.toString()); + } else if (ObjectFieldType.DATE.equals(column.getObjectFieldType())) { + value = Convert.toLocalDateTime(value); + } + } + return value; + } + + private boolean isAutoSettingField(OnlineColumn column) { + return ObjectUtil.equal(column.getFieldKind(), FieldKind.CREATE_TIME) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.CREATE_USER_ID) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.UPDATE_TIME) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.UPDATE_USER_ID) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.CREATE_DEPT_ID) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.LOGIC_DELETE); + } + + private boolean canDownload(String fieldJsonData, String filename) { + if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) { + return false; + } + return BaseUpDownloader.containFile(fieldJsonData, filename) + || cacheHelper.existSessionUploadFile(filename); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java new file mode 100644 index 00000000..431ae946 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java @@ -0,0 +1,76 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单 Redis 键生成工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineRedisKeyUtil { + + /** + * 计算在线表对象缓存在Redis中的键值。 + * + * @param tableId 在线表主键Id。 + * @return 在线表对象缓存在Redis中的键值。 + */ + public static String makeOnlineTableKey(Long tableId) { + return "ONLINE_TABLE:" + tableId; + } + + /** + * 计算在线表单对象缓存在Redis中的键值。 + * + * @param formId 在线表单对象主键Id。 + * @return 在线表单对象缓存在Redis中的键值。 + */ + public static String makeOnlineFormKey(Long formId) { + return "ONLINE_FORM:" + formId; + } + + /** + * 计算在线表单关联数据源对象列表缓存在Redis中的键值。 + * + * @param formId 在线表单对象主键Id。 + * @return 在线表单关联数据源对象列表缓存在Redis中的键值。 + */ + public static String makeOnlineFormDatasourceKey(Long formId) { + return "ONLINE_FORM_DATASOURCE_LIST:" + formId; + } + + /** + * 计算在线数据源对象缓存在Redis中的键值。 + * + * @param datasourceId 在线数据源主键Id。 + * @return 在线数据源对象缓存在Redis中的键值。 + */ + public static String makeOnlineDataSourceKey(Long datasourceId) { + return "ONLINE_DATASOURCE:" + datasourceId; + } + + /** + * 计算在线数据源关联列表对象缓存在Redis中的键值。 + * + * @param datasourceId 在线数据源主键Id。 + * @return 在线数据源关联列表对象缓存在Redis中的键值。 + */ + public static String makeOnlineDataSourceRelationKey(Long datasourceId) { + return "ONLINE_DATASOURCE_RELATION:" + datasourceId; + } + + /** + * 计算在线字典对象缓存在Redis中的键值。 + * + * @param dictId 在线字典主键Id。 + * @return 在线字典对象缓存在Redis中的键值。 + */ + public static String makeOnlineDictKey(Long dictId) { + return "ONLINE_DICT:" + dictId; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private OnlineRedisKeyUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java new file mode 100644 index 00000000..712fe312 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineUtil { + + /** + * 根据输入参数,拼接在线表单操作的查看权限字。 + * + * @param datasourceVariableName 数据源变量名。 + * @return 拼接后的在线表单操作的查看权限字。 + */ + public static String makeViewPermCode(String datasourceVariableName) { + return "online:" + datasourceVariableName + ":view"; + } + + /** + * 根据输入参数,拼接在线表单操作的编辑权限字。 + * + * @param datasourceVariableName 数据源变量名。 + * @return 拼接后的在线表单操作的编辑权限字。 + */ + public static String makeEditPermCode(String datasourceVariableName) { + return "online:" + datasourceVariableName + ":edit"; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private OnlineUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java new file mode 100644 index 00000000..677eb67a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 在线表单数据表字段规则和字段多对多关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段规则和字段多对多关联VO对象") +@Data +public class OnlineColumnRuleVo { + + /** + * 字段Id。 + */ + @Schema(description = "字段Id") + private Long columnId; + + /** + * 规则Id。 + */ + @Schema(description = "规则Id") + private Long ruleId; + + /** + * 规则属性数据。 + */ + @Schema(description = "规则属性数据") + private String propDataJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java new file mode 100644 index 00000000..3438eed4 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java @@ -0,0 +1,204 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段规则和字段多对多关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段规则和字段多对多关联VO对象") +@Data +public class OnlineColumnVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long columnId; + + /** + * 字段名。 + */ + @Schema(description = "字段名") + private String columnName; + + /** + * 数据表Id。 + */ + @Schema(description = "数据表Id") + private Long tableId; + + /** + * 数据表中的字段类型。 + */ + @Schema(description = "数据表中的字段类型") + private String columnType; + + /** + * 数据表中的完整字段类型(包括了精度和刻度)。 + */ + @Schema(description = "数据表中的完整字段类型") + private String fullColumnType; + + /** + * 是否为主键。 + */ + @Schema(description = "是否为主键") + private Boolean primaryKey; + + /** + * 是否是自增主键(0: 不是 1: 是)。 + */ + @Schema(description = "是否是自增主键") + private Boolean autoIncrement; + + /** + * 是否可以为空 (0: 不可以为空 1: 可以为空)。 + */ + @Schema(description = "是否可以为空") + private Boolean nullable; + + /** + * 缺省值。 + */ + @Schema(description = "缺省值") + private String columnDefault; + + /** + * 字段在数据表中的显示位置。 + */ + @Schema(description = "字段在数据表中的显示位置") + private Integer columnShowOrder; + + /** + * 数据表中的字段注释。 + */ + @Schema(description = "数据表中的字段注释") + private String columnComment; + + /** + * 对象映射字段名称。 + */ + @Schema(description = "对象映射字段名称") + private String objectFieldName; + + /** + * 对象映射字段类型。 + */ + @Schema(description = "对象映射字段类型") + private String objectFieldType; + + /** + * 数值型字段的精度(目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的精度") + private Integer numericPrecision; + + /** + * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的刻度") + private Integer numericScale; + + /** + * 过滤类型。 + */ + @Schema(description = "过滤类型") + private Integer filterType; + + /** + * 是否是主键的父Id。 + */ + @Schema(description = "是否是主键的父Id") + private Boolean parentKey; + + /** + * 是否部门过滤字段。 + */ + @Schema(description = "是否部门过滤字段") + private Boolean deptFilter; + + /** + * 是否用户过滤字段。 + */ + @Schema(description = "是否用户过滤字段") + private Boolean userFilter; + + /** + * 字段类别。 + */ + @Schema(description = "字段类别") + private Integer fieldKind; + + /** + * 包含的文件文件数量,0表示无限制。 + */ + @Schema(description = "包含的文件文件数量,0表示无限制") + private Integer maxFileCount; + + /** + * 上传文件系统类型。 + */ + @Schema(description = "上传文件系统类型") + private Integer uploadFileSystemType; + + /** + * 编码规则的JSON格式数据。 + */ + @Schema(description = "编码规则的JSON格式数据") + private String encodedRule; + + /** + * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 + */ + @Schema(description = "脱敏字段类型") + private String maskFieldType; + + /** + * 字典Id。 + */ + @Schema(description = "字典Id") + private Long dictId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * fieldKind 常量字典关联数据。 + */ + @Schema(description = "常量字典关联数据") + private Map fieldKindDictMap; + + /** + * dictId 的一对一关联。 + */ + @Schema(description = "dictId 的一对一关联") + private Map dictInfo; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java new file mode 100644 index 00000000..6af755a9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java @@ -0,0 +1,150 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单的数据源关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源关联VO对象") +@Data +public class OnlineDatasourceRelationVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long relationId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 关联名称。 + */ + @Schema(description = "关联名称") + private String relationName; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + private String variableName; + + /** + * 主数据源Id。 + */ + @Schema(description = "主数据源Id") + private Long datasourceId; + + /** + * 关联类型。 + */ + @Schema(description = "关联类型") + private Integer relationType; + + /** + * 主表关联字段Id。 + */ + @Schema(description = "主表关联字段Id") + private Long masterColumnId; + + /** + * 从表Id。 + */ + @Schema(description = "从表Id") + private Long slaveTableId; + + /** + * 从表关联字段Id。 + */ + @Schema(description = "从表关联字段Id") + private Long slaveColumnId; + + /** + * 删除主表的时候是否级联删除一对一和一对多的从表数据,多对多只是删除关联,不受到这个标记的影响。。 + */ + @Schema(description = "一对多从表级联删除标记") + private Boolean cascadeDelete; + + /** + * 是否左连接。 + */ + @Schema(description = "是否左连接") + private Boolean leftJoin; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * masterColumnId 的一对一关联数据对象,数据对应类型为OnlineColumnVo。 + */ + @Schema(description = "masterColumnId字段的一对一关联数据对象") + private Map masterColumn; + + /** + * slaveTableId 的一对一关联数据对象,数据对应类型为OnlineTableVo。 + */ + @Schema(description = "slaveTableId字段的一对一关联数据对象") + private Map slaveTable; + + /** + * slaveColumnId 的一对一关联数据对象,数据对应类型为OnlineColumnVo。 + */ + @Schema(description = "slaveColumnId字段的一对一关联数据对象") + private Map slaveColumn; + + /** + * masterColumnId 字典关联数据。 + */ + @Schema(description = "masterColumnId的字典关联数据") + private Map masterColumnIdDictMap; + + /** + * slaveTableId 字典关联数据。 + */ + @Schema(description = "slaveTableId的字典关联数据") + private Map slaveTableIdDictMap; + + /** + * slaveColumnId 字典关联数据。 + */ + @Schema(description = "slaveColumnId的字典关联数据") + private Map slaveColumnIdDictMap; + + /** + * relationType 常量字典关联数据。 + */ + @Schema(description = "常量字典关联数据") + private Map relationTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java new file mode 100644 index 00000000..bbf8c2bc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java @@ -0,0 +1,98 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单的数据源VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源VO对象") +@Data +public class OnlineDatasourceVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long datasourceId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 数据源名称。 + */ + @Schema(description = "数据源名称") + private String datasourceName; + + /** + * 数据源变量名,会成为数据访问url的一部分。 + */ + @Schema(description = "数据源变量名") + private String variableName; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 主表Id。 + */ + @Schema(description = "主表Id") + private Long masterTableId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * datasourceId 的多对多关联表数据对象,数据对应类型为OnlinePageDatasource。 + */ + @Schema(description = "datasourceId 的多对多关联表数据对象,数据对应类型为OnlinePageDatasource") + private Map onlinePageDatasource; + + /** + * masterTableId 字典关联数据。 + */ + @Schema(description = "masterTableId 字典关联数据") + private Map masterTableIdDictMap; + + /** + * 当前数据源及其关联,引用的数据表对象列表。 + */ + @Schema(description = "当前数据源及其关联,引用的数据表对象列表") + private List tableList; +} +//、FlowTaskTimeoutTimer \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java new file mode 100644 index 00000000..6415f31c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java @@ -0,0 +1,84 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表所在数据库链接VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表所在数据库链接VO对象") +@Data +public class OnlineDblinkVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long dblinkId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 链接中文名称。 + */ + @Schema(description = "链接中文名称") + private String dblinkName; + + /** + * 链接描述。 + */ + @Schema(description = "链接描述") + private String dblinkDescription; + + /** + * 配置信息。 + */ + @Schema(description = "配置信息") + private String configuration; + + /** + * 数据库链接类型。 + */ + @Schema(description = "数据库链接类型") + private Integer dblinkType; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 数据库链接类型常量字典关联数据。 + */ + @Schema(description = "数据库链接类型常量字典关联数据") + private Map dblinkTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java new file mode 100644 index 00000000..804e5c71 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java @@ -0,0 +1,162 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单关联的字典VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单关联的字典VO对象") +@Data +public class OnlineDictVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long dictId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 字典名称。 + */ + @Schema(description = "字典名称") + private String dictName; + + /** + * 字典类型。 + */ + @Schema(description = "字典类型") + private Integer dictType; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 字典表名称。 + */ + @Schema(description = "字典表名称") + private String tableName; + + /** + * 全局字典编码。 + */ + @Schema(description = "全局字典编码") + private String dictCode; + + /** + * 逻辑删除字段。 + */ + @Schema(description = "逻辑删除字段") + private String deletedColumnName; + + /** + * 用户过滤滤字段名称。 + */ + @Schema(description = "用户过滤滤字段名称") + private String userFilterColumnName; + + /** + * 部门过滤字段名称。 + */ + @Schema(description = "部门过滤字段名称") + private String deptFilterColumnName; + + /** + * 租户过滤字段名称。 + */ + @Schema(description = "租户过滤字段名称") + private String tenantFilterColumnName; + + /** + * 字典表键字段名称。 + */ + @Schema(description = "字典表键字段名称") + private String keyColumnName; + + /** + * 字典表父键字段名称。 + */ + @Schema(description = "字典表父键字段名称") + private String parentKeyColumnName; + + /** + * 字典值字段名称。 + */ + @Schema(description = "字典值字段名称") + private String valueColumnName; + + /** + * 是否树形标记。 + */ + @Schema(description = "是否树形标记") + private Boolean treeFlag; + + /** + * 获取字典数据的url。 + */ + @Schema(description = "获取字典数据的url") + private String dictListUrl; + + /** + * 根据主键id批量获取字典数据的url。 + */ + @Schema(description = "根据主键id批量获取字典数据的url") + private String dictIdsUrl; + + /** + * 字典的JSON数据。 + */ + @Schema(description = "字典的JSON数据") + private String dictDataJson; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * dictType 常量字典关联数据。 + */ + @Schema(description = "dictType 常量字典关联数据") + private Map dictTypeDictMap; + + /** + * 数据库链接Id字典关联数据。 + */ + @Schema(description = "数据库链接Id字典关联数据") + private Map dblinkIdDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java new file mode 100644 index 00000000..d3373ce8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java @@ -0,0 +1,127 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单VO对象") +@Data +public class OnlineFormVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long formId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 页面Id。 + */ + @Schema(description = "页面Id") + private Long pageId; + + /** + * 表单编码。 + */ + @Schema(description = "表单编码") + private String formCode; + + /** + * 表单名称。 + */ + @Schema(description = "表单名称") + private String formName; + + /** + * 表单类型。 + */ + @Schema(description = "表单类型") + private Integer formType; + + /** + * 表单类别。 + */ + @Schema(description = "表单类别") + private Integer formKind; + + /** + * 表单主表Id。 + */ + @Schema(description = "表单主表Id") + private Long masterTableId; + + /** + * 表单组件JSON。 + */ + @Schema(description = "表单组件JSON") + private String widgetJson; + + /** + * 表单参数JSON。 + */ + @Schema(description = "表单参数JSON") + private String paramsJson; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * masterTableId 的一对一关联数据对象,数据对应类型为OnlineTableVo。 + */ + @Schema(description = "asterTableId 的一对一关联数据对象") + private Map onlineTable; + + /** + * masterTableId 字典关联数据。 + */ + @Schema(description = "masterTableId 字典关联数据") + private Map masterTableIdDictMap; + + /** + * formType 常量字典关联数据。 + */ + @Schema(description = "formType 常量字典关联数据") + private Map formTypeDictMap; + + /** + * 当前表单关联的数据源Id集合。 + */ + @Schema(description = "当前表单关联的数据源Id集合") + private List datasourceIdList; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java new file mode 100644 index 00000000..adb113ff --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 在线表单页面和数据源多对多关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单页面和数据源多对多关联VO对象") +@Data +public class OnlinePageDatasourceVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long id; + + /** + * 页面主键Id。 + */ + @Schema(description = "页面主键Id") + private Long pageId; + + /** + * 数据源主键Id。 + */ + @Schema(description = "数据源主键Id") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java new file mode 100644 index 00000000..bd80de12 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java @@ -0,0 +1,96 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单所在页面VO对象。这里我们可以把页面理解为表单的容器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单所在页面VO对象") +@Data +public class OnlinePageVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long pageId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 页面编码。 + */ + @Schema(description = "页面编码") + private String pageCode; + + /** + * 页面名称。 + */ + @Schema(description = "页面名称") + private String pageName; + + /** + * 页面类型。 + */ + @Schema(description = "页面类型") + private Integer pageType; + + /** + * 页面编辑状态。 + */ + @Schema(description = "页面编辑状态") + private Integer status; + + /** + * 是否发布。 + */ + @Schema(description = "是否发布") + private Boolean published; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * pageType 常量字典关联数据。 + */ + @Schema(description = "pageType 常量字典关联数据") + private Map pageTypeDictMap; + + /** + * status 常量字典关联数据。 + */ + @Schema(description = "status 常量字典关联数据") + private Map statusDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java new file mode 100644 index 00000000..ba88dbec --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java @@ -0,0 +1,90 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段验证规则VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段验证规则VO对象") +@Data +public class OnlineRuleVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long ruleId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 规则名称。 + */ + @Schema(description = "规则名称") + private String ruleName; + + /** + * 规则类型。 + */ + @Schema(description = "规则类型") + private Integer ruleType; + + /** + * 内置规则标记。 + */ + @Schema(description = "内置规则标记") + private Boolean builtin; + + /** + * 自定义规则的正则表达式。 + */ + @Schema(description = "自定义规则的正则表达式") + private String pattern; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * ruleId 的多对多关联表数据对象,数据对应类型为OnlineColumnRuleVo。 + */ + @Schema(description = "ruleId 的多对多关联表数据对象") + private Map onlineColumnRule; + + /** + * ruleType 常量字典关联数据。 + */ + @Schema(description = "ruleType 常量字典关联数据") + private Map ruleTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java new file mode 100644 index 00000000..66561baf --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 在线表单的数据表VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据表VO对象") +@Data +public class OnlineTableVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long tableId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 表名称。 + */ + @Schema(description = "表名称") + private String tableName; + + /** + * 实体名称。 + */ + @Schema(description = "实体名称") + private String modelName; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java new file mode 100644 index 00000000..2a4ca215 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 在线数据表虚拟字段VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线数据表虚拟字段VO对象") +@Data +public class OnlineVirtualColumnVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long virtualColumnId; + + /** + * 所在表Id。 + */ + @Schema(description = "所在表Id") + private Long tableId; + + /** + * 字段名称。 + */ + @Schema(description = "字段名称") + private String objectFieldName; + + /** + * 属性类型。 + */ + @Schema(description = "属性类型") + private String objectFieldType; + + /** + * 字段提示名。 + */ + @Schema(description = "字段提示名") + private String columnPrompt; + + /** + * 虚拟字段类型(0: 聚合)。 + */ + @Schema(description = "虚拟字段类型(0: 聚合)") + private Integer virtualType; + + /** + * 关联数据源Id。 + */ + @Schema(description = "关联数据源Id") + private Long datasourceId; + + /** + * 关联Id。 + */ + @Schema(description = "关联Id") + private Long relationId; + + /** + * 聚合字段所在关联表Id。 + */ + @Schema(description = "聚合字段所在关联表Id") + private Long aggregationTableId; + + /** + * 关联表聚合字段Id。 + */ + @Schema(description = "关联表聚合字段Id") + private Long aggregationColumnId; + + /** + * 聚合类型(0: count 1: sum 2: avg 3: max 4:min)。 + */ + @Schema(description = "聚合类型(0: count 1: sum 2: avg 3: max 4:min)") + private Integer aggregationType; + + /** + * 存储过滤条件的json。 + */ + @Schema(description = "存储过滤条件的json") + private String whereClauseJson; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..d9cb5fb0 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.online.config.OnlineAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-redis/pom.xml new file mode 100644 index 00000000..c0fe169d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-redis + 1.0.0 + common-redis + jar + + + + com.orangeforms + common-core + 1.0.0 + + + org.redisson + redisson + ${redisson.version} + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java new file mode 100644 index 00000000..da1c2fc2 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java @@ -0,0 +1,263 @@ +package com.orangeforms.common.redis.cache; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.cache.DictionaryCache; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.RedisCacheAccessException; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 字典数据Redis缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class RedisDictionaryCache implements DictionaryCache { + + /** + * 字典数据前缀,便于Redis工具分组显示。 + */ + protected static final String DICT_PREFIX = "DICT-TABLE:"; + /** + * redisson客户端。 + */ + protected final RedissonClient redissonClient; + /** + * 数据存储对象。 + */ + protected final RMap dataMap; + /** + * 字典值对象类型。 + */ + protected final Class valueClazz; + /** + * 获取字典主键数据的函数对象。 + */ + protected final Function idGetter; + + /** + * 当前对象的构造器函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的字典内存缓存对象。 + */ + public static RedisDictionaryCache create( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + return new RedisDictionaryCache<>(redissonClient, dictionaryName, valueClazz, idGetter); + } + + /** + * 构造函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。确保全局唯一。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + */ + public RedisDictionaryCache( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter) { + this.redissonClient = redissonClient; + this.dataMap = redissonClient.getMap( + DICT_PREFIX + dictionaryName + ApplicationConstant.DICT_CACHE_NAME_SUFFIX); + this.valueClazz = valueClazz; + this.idGetter = idGetter; + } + + protected RMap getDataMap() { + return dataMap; + } + + @Override + public List getAll() { + Collection dataList; + String exceptionMessage; + try { + dataList = getDataMap().readAllValues(); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::getAll] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (dataList == null) { + return new LinkedList<>(); + } + return dataList.stream() + .map(data -> JSON.parseObject(data, valueClazz)) + .collect(Collectors.toCollection(LinkedList::new)); + } + + @Override + public List getInList(Set keys) { + if (CollUtil.isEmpty(keys)) { + return new LinkedList<>(); + } + Collection dataList; + String exceptionMessage; + try { + dataList = getDataMap().getAll(keys).values(); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::getInList] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (dataList == null) { + return new LinkedList<>(); + } + return dataList.stream() + .map(data -> JSON.parseObject(data, valueClazz)) + .collect(Collectors.toCollection(LinkedList::new)); + } + + @Override + public V get(K id) { + if (id == null) { + return null; + } + String data; + String exceptionMessage; + try { + data = getDataMap().get(id); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::get] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (data == null) { + return null; + } + return JSON.parseObject(data, valueClazz); + } + + @Override + public int getCount() { + return getDataMap().size(); + } + + @Override + public void put(K id, V data) { + if (id == null || data == null) { + return; + } + String exceptionMessage; + try { + getDataMap().fastPut(id, JSON.toJSONString(data)); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::put] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void reload(List dataList, boolean force) { + String exceptionMessage; + try { + // 如果不强制刷新,需要先判断缓存中是否存在数据。 + if (!force && this.getCount() > 0) { + return; + } + Map map = null; + if (CollUtil.isNotEmpty(dataList)) { + map = dataList.stream().collect(Collectors.toMap(idGetter, JSON::toJSONString)); + } + RMap localDataMap = getDataMap(); + localDataMap.clear(); + if (map != null) { + localDataMap.putAll(map); + } + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::reload] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + String data; + String exceptionMessage; + try { + data = getDataMap().remove(id); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::invalidate] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (data == null) { + return null; + } + return JSON.parseObject(data, valueClazz); + } + + @SuppressWarnings("unchecked") + @Override + public void invalidateSet(Set keys) { + if (CollUtil.isEmpty(keys)) { + return; + } + Object[] keyArray = keys.toArray(new Object[]{}); + String exceptionMessage; + try { + getDataMap().fastRemove((K[]) keyArray); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::invalidateSet] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void invalidateAll() { + String exceptionMessage; + try { + getDataMap().clear(); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::invalidateAll] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java new file mode 100644 index 00000000..de910c61 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java @@ -0,0 +1,224 @@ +package com.orangeforms.common.redis.cache; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.RedisCacheAccessException; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import org.redisson.api.RListMultimap; +import org.redisson.api.RedissonClient; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 树形字典数据Redis缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class RedisTreeDictionaryCache extends RedisDictionaryCache { + + /** + * 树形数据存储对象。 + */ + private final RListMultimap allTreeMap; + /** + * 获取字典父主键数据的函数对象。 + */ + protected final Function parentIdGetter; + + /** + * 当前对象的构造器函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的树形字典内存缓存对象。 + */ + public static RedisTreeDictionaryCache create( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter, + Function parentIdGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + if (parentIdGetter == null) { + throw new IllegalArgumentException("ParentIdGetter can't be NULL."); + } + return new RedisTreeDictionaryCache<>( + redissonClient, dictionaryName, valueClazz, idGetter, parentIdGetter); + } + + /** + * 构造函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + */ + public RedisTreeDictionaryCache( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter, + Function parentIdGetter) { + super(redissonClient, dictionaryName, valueClazz, idGetter); + this.allTreeMap = redissonClient.getListMultimap( + DICT_PREFIX + dictionaryName + ApplicationConstant.TREE_DICT_CACHE_NAME_SUFFIX); + this.parentIdGetter = parentIdGetter; + } + + @Override + public List getListByParentId(K parentId) { + List dataList; + String exceptionMessage; + try { + dataList = allTreeMap.get(parentId); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::getListByParentId] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(data -> JSON.parseObject(data, valueClazz)).collect(Collectors.toList()); + } + + @Override + public void reload(List dataList, boolean force) { + String exceptionMessage; + try { + // 如果不强制刷新,需要先判断缓存中是否存在数据。 + if (!force && this.getCount() > 0) { + return; + } + dataMap.clear(); + allTreeMap.clear(); + if (CollUtil.isEmpty(dataList)) { + return; + } + Map map = dataList.stream().collect(Collectors.toMap(idGetter, JSON::toJSONString)); + // 这里现在本地内存构建树形数据关系,然后再批量存入到Redis缓存。 + // 以便减少与Redis的交互,同时提升运行时效率。 + Multimap treeMap = LinkedListMultimap.create(); + for (V data : dataList) { + treeMap.put(parentIdGetter.apply(data), JSON.toJSONString(data)); + } + dataMap.putAll(map, 3000); + for (Map.Entry> entry : treeMap.asMap().entrySet()) { + allTreeMap.putAll(entry.getKey(), entry.getValue()); + } + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisDictionaryCache::reload] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void put(K id, V data) { + if (id == null || data == null) { + return; + } + String stringData = JSON.toJSONString(data); + K parentId = parentIdGetter.apply(data); + String exceptionMessage; + try { + String oldData = dataMap.put(id, stringData); + if (oldData != null) { + allTreeMap.remove(parentId, oldData); + } + allTreeMap.put(parentId, stringData); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::put] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + V data = null; + String exceptionMessage; + try { + String stringData = dataMap.remove(id); + if (stringData != null) { + data = JSON.parseObject(stringData, valueClazz); + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, stringData); + } + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::invalidate] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + return data; + } + + @Override + public void invalidateSet(Set keys) { + if (CollUtil.isEmpty(keys)) { + return; + } + String exceptionMessage; + try { + keys.forEach(id -> { + if (id != null) { + String stringData = dataMap.remove(id); + if (stringData != null) { + K parentId = parentIdGetter.apply(JSON.parseObject(stringData, valueClazz)); + allTreeMap.remove(parentId, stringData); + } + } + }); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::invalidateSet] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void invalidateAll() { + String exceptionMessage; + try { + dataMap.clear(); + allTreeMap.clear(); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::invalidateAll] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java new file mode 100644 index 00000000..5210be88 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java @@ -0,0 +1,73 @@ +package com.orangeforms.common.redis.cache; + +import com.google.common.collect.Maps; +import org.redisson.api.RedissonClient; +import org.redisson.spring.cache.CacheConfig; +import org.redisson.spring.cache.RedissonSpringCacheManager; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.util.Map; + +/** + * 使用Redisson作为Redis的分布式缓存库。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@EnableCaching +public class RedissonCacheConfig { + + private static final int DEFAULT_TTL = 3600000; + + /** + * 定义cache名称、超时时长(毫秒)。 + */ + public enum CacheEnum { + /** + * session下上传文件名的缓存(时间是24小时)。 + */ + UPLOAD_FILENAME_CACHE(86400000), + /** + * session的打印访问令牌缓存(时间是1小时)。 + */ + PRINT_ACCESS_TOKEN_CACHE(3600000), + /** + * 缺省全局缓存(时间是24小时)。 + */ + GLOBAL_CACHE(86400000); + + /** + * 缓存的时长(单位:毫秒) + */ + private int ttl = DEFAULT_TTL; + + CacheEnum() { + } + + CacheEnum(int ttl) { + this.ttl = ttl; + } + + public int getTtl() { + return ttl; + } + } + + /** + * 初始化缓存配置。 + */ + @Bean + @Primary + public CacheManager cacheManager(RedissonClient redissonClient) { + Map config = Maps.newHashMap(); + for (CacheEnum c : CacheEnum.values()) { + config.put(c.name(), new CacheConfig(c.getTtl(), 0)); + } + return new RedissonSpringCacheManager(redissonClient, config); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java new file mode 100644 index 00000000..4c613c7c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java @@ -0,0 +1,179 @@ +package com.orangeforms.common.redis.cache; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.object.MyPrintInfo; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.exception.MyRuntimeException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Session数据缓存辅助类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@SuppressWarnings("unchecked") +@Component +public class SessionCacheHelper { + + @Autowired + private CacheManager cacheManager; + + private static final String NO_CACHE_FORMAT_MSG = "No redisson cache [{}]!"; + + /** + * 缓存当前session内,上传过的文件名。 + * + * @param filename 通常是本地存储的文件名,而不是上传时的原始文件名。 + */ + public void putSessionUploadFile(String filename) { + if (filename != null) { + Set sessionUploadFileSet = null; + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + throw new MyRuntimeException(msg); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper != null) { + sessionUploadFileSet = (Set) valueWrapper.get(); + } + if (sessionUploadFileSet == null) { + sessionUploadFileSet = new HashSet<>(); + } + sessionUploadFileSet.add(filename); + cache.put(TokenData.takeFromRequest().getSessionId(), sessionUploadFileSet); + } + } + + /** + * 缓存当前Session可以下载的文件集合。 + * + * @param filenameSet 后台服务本地存储的文件名,而不是上传时的原始文件名。 + */ + public void putSessionDownloadableFileNameSet(Set filenameSet) { + if (CollUtil.isEmpty(filenameSet)) { + return; + } + Set sessionUploadFileSet = null; + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + if (cache == null) { + throw new MyRuntimeException(StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name())); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper != null) { + sessionUploadFileSet = (Set) valueWrapper.get(); + } + if (sessionUploadFileSet == null) { + sessionUploadFileSet = new HashSet<>(); + } + sessionUploadFileSet.addAll(filenameSet); + cache.put(TokenData.takeFromRequest().getSessionId(), sessionUploadFileSet); + } + + /** + * 判断参数中的文件名,是否有当前session上传。 + * + * @param filename 通常是本地存储的文件名,而不是上传时的原始文件名。 + * @return true表示该文件是由当前session上传并存储在本地的,否则false。 + */ + public boolean existSessionUploadFile(String filename) { + if (filename == null) { + return false; + } + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + throw new MyRuntimeException(msg); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper == null) { + return false; + } + Object cachedData = valueWrapper.get(); + if (cachedData == null) { + return false; + } + return ((Set) cachedData).contains(filename); + } + + /** + * 缓存当前session内,可打印的安全令牌。 + * + * @param token 打印安全令牌。 + * @param printInfo 打印参数信息。 + */ + public void putSessionPrintTokenAndInfo(String token, MyPrintInfo printInfo) { + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + throw new MyRuntimeException(msg); + } + Map sessionPrintTokenMap = null; + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper != null) { + sessionPrintTokenMap = (Map) valueWrapper.get(); + } + if (sessionPrintTokenMap == null) { + sessionPrintTokenMap = new HashMap<>(4); + } + sessionPrintTokenMap.put(token, JSON.toJSONString(printInfo)); + cache.put(TokenData.takeFromRequest().getSessionId(), sessionPrintTokenMap); + } + + /** + * 获取当前session中,指定打印令牌所关联的打印信息。 + * + * @param token 打印安全令牌。 + * @return 当前session中,指定打印令牌所关联的打印信息。不存在返回null。 + */ + public MyPrintInfo getSessionPrintInfoByToken(String token) { + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + throw new MyRuntimeException(msg); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper == null) { + return null; + } + Object cachedData = valueWrapper.get(); + if (cachedData == null) { + return null; + } + String data = ((Map) cachedData).get(token); + if (data == null) { + return null; + } + return JSON.parseObject(data, MyPrintInfo.class); + } + + /** + * 清除当前session的所有缓存数据。 + * + * @param sessionId 当前会话的SessionId。 + */ + public void removeAllSessionCache(String sessionId) { + for (RedissonCacheConfig.CacheEnum c : RedissonCacheConfig.CacheEnum.values()) { + Cache cache = cacheManager.getCache(c.name()); + if (cache != null) { + cache.evict(sessionId); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java new file mode 100644 index 00000000..fecec4b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java @@ -0,0 +1,105 @@ +package com.orangeforms.common.redis.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.exception.InvalidRedisModeException; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Redisson配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@ConditionalOnProperty(name = "common-redis.redisson.enabled", havingValue = "true") +public class RedissonConfig { + + @Value("${common-redis.redisson.lockWatchdogTimeout}") + private Integer lockWatchdogTimeout; + + @Value("${common-redis.redisson.mode}") + private String mode; + + /** + * 仅仅用于sentinel模式。 + */ + @Value("${common-redis.redisson.masterName:}") + private String masterName; + + @Value("${common-redis.redisson.address}") + private String address; + + @Value("${common-redis.redisson.timeout}") + private Integer timeout; + + @Value("${common-redis.redisson.password:}") + private String password; + + @Value("${common-redis.redisson.pool.poolSize}") + private Integer poolSize; + + @Value("${common-redis.redisson.pool.minIdle}") + private Integer minIdle; + + @Bean + public RedissonClient redissonClient() { + if (StrUtil.isBlank(password)) { + password = null; + } + Config config = new Config(); + if ("single".equals(mode)) { + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useSingleServer() + .setPassword(password) + .setAddress(address) + .setConnectionPoolSize(poolSize) + .setConnectionMinimumIdleSize(minIdle) + .setConnectTimeout(timeout); + } else if ("cluster".equals(mode)) { + String[] clusterAddresses = StrUtil.splitToArray(address, ','); + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useClusterServers() + .setPassword(password) + .addNodeAddress(clusterAddresses) + .setConnectTimeout(timeout) + .setMasterConnectionPoolSize(poolSize) + .setMasterConnectionMinimumIdleSize(minIdle); + } else if ("sentinel".equals(mode)) { + String[] sentinelAddresses = StrUtil.splitToArray(address, ','); + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useSentinelServers() + .setPassword(password) + .setMasterName(masterName) + .addSentinelAddress(sentinelAddresses) + .setConnectTimeout(timeout) + .setMasterConnectionPoolSize(poolSize) + .setMasterConnectionMinimumIdleSize(minIdle); + } else if ("master-slave".equals(mode)) { + String[] masterSlaveAddresses = StrUtil.splitToArray(address, ','); + if (masterSlaveAddresses.length == 1) { + throw new IllegalArgumentException( + "redis.redisson.address MUST have multiple redis addresses for master-slave mode."); + } + String[] slaveAddresses = new String[masterSlaveAddresses.length - 1]; + ArrayUtil.copy(masterSlaveAddresses, 1, slaveAddresses, 0, slaveAddresses.length); + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useMasterSlaveServers() + .setPassword(password) + .setMasterAddress(masterSlaveAddresses[0]) + .addSlaveAddress(slaveAddresses) + .setConnectTimeout(timeout) + .setMasterConnectionPoolSize(poolSize) + .setMasterConnectionMinimumIdleSize(minIdle); + } else { + throw new InvalidRedisModeException(mode); + } + return Redisson.create(config); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java new file mode 100644 index 00000000..7caae85c --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java @@ -0,0 +1,216 @@ +package com.orangeforms.common.redis.util; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RAtomicLong; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * Redis的常用工具方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class CommonRedisUtil { + + @Autowired + private RedissonClient redissonClient; + + private static final Integer DEFAULT_EXPIRE_SECOND = 300; + + /** + * 计算流水号前缀部分。 + * + * @param prefix 前缀字符串。 + * @param precisionTo 精确到的时间单元,目前仅仅支持 YEAR/MONTH/DAYS/HOURS/MINUTES/SECONDS。 + * @param middle 日期和流水号之间的字符串。 + * @return 返回计算后的前缀部分。 + */ + public String calculateTransIdPrefix(String prefix, String precisionTo, String middle) { + String key = prefix; + if (key == null) { + key = ""; + } + DateTime dateTime = new DateTime(); + String fmt = "yyyy"; + String fmt2 = fmt + "MMddHH"; + switch (precisionTo) { + case "YEAR": + break; + case "MONTH": + fmt += "MM"; + break; + case "DAYS": + fmt = fmt + "MMdd"; + break; + case "HOURS": + fmt = fmt2; + break; + case "MINUTES": + fmt = fmt2 + "mm"; + break; + case "SECONDS": + fmt = fmt2 + "mmss"; + break; + default: + throw new UnsupportedOperationException("Only Support YEAR/MONTH/DAYS/HOURS/MINUTES/SECONDS"); + } + key += dateTime.toString(fmt); + return middle != null ? key + middle : key; + } + + /** + * 生成基于时间的流水号方法。 + * + * @param prefix 前缀字符串。 + * @param precisionTo 精确到的时间单元,目前仅仅支持 YEAR/MONTH/DAYS/HOURS/MINUTES/SECONDS。 + * @param middle 日期和流水号之间的字符串。 + * @param idWidth 计算出的流水号宽度,前面补充0。比如idWidth = 3, 输出值为 005/012/123。 + * 需要注意的是,流水号值超出idWidth指定宽度,低位会被截取。 + * @return 基于时间的流水号方法。 + */ + public String generateTransId(String prefix, String precisionTo, String middle, int idWidth) { + TimeUnit unit = EnumUtil.fromString(TimeUnit.class, precisionTo, null); + int unitCount = 1; + if (unit == null) { + unit = TimeUnit.DAYS; + DateTime now = DateTime.now(); + if (StrUtil.equals(precisionTo, "MONTH")) { + DateTime endOfMonthDay = DateUtil.endOfMonth(now); + unitCount = endOfMonthDay.getField(DateField.DAY_OF_MONTH) - now.getField(DateField.DAY_OF_MONTH) + 1; + } else if (StrUtil.equals(precisionTo, "YEAR")) { + DateTime endOfYearDay = DateUtil.endOfYear(now); + unitCount = endOfYearDay.getField(DateField.DAY_OF_YEAR) - now.getField(DateField.DAY_OF_YEAR) + 1; + } + } + String key = this.calculateTransIdPrefix(prefix, precisionTo, middle); + RAtomicLong atomicLong = redissonClient.getAtomicLong(key); + long value = atomicLong.incrementAndGet(); + if (value == 1L) { + atomicLong.expire(unitCount, unit); + } + return key + StrUtil.padPre(String.valueOf(value), idWidth, "0"); + } + + /** + * 为指定的键设置流水号的初始值。 + * + * @param key 指定的键。 + * @param initalValue 初始值。 + */ + public void initTransId(String key, Long initalValue) { + RAtomicLong atomicLong = redissonClient.getAtomicLong(key); + atomicLong.set(initalValue); + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param id 数据Id。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @return 数据对象。 + */ + public M getFromCache(String key, Serializable id, Function f, Class clazz) { + return this.getFromCache(key, id, f, clazz, null); + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param filter mybatis flex的过滤对象。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @return 数据对象。 + */ + public N getFromCacheWithQueryWrapper(String key, QueryWrapper filter, Function f, Class clazz) { + N m; + RBucket bucket = redissonClient.getBucket(key); + if (!bucket.isExists()) { + m = f.apply(filter); + if (m != null) { + bucket.set(JSON.toJSONString(m), DEFAULT_EXPIRE_SECOND, TimeUnit.SECONDS); + } + } else { + m = JSON.parseObject(bucket.get(), clazz); + } + return m; + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param filter 过滤对象。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @return 数据对象。 + */ + public N getFromCache(String key, M filter, Function f, Class clazz) { + N m; + RBucket bucket = redissonClient.getBucket(key); + if (!bucket.isExists()) { + m = f.apply(filter); + if (m != null) { + bucket.set(JSON.toJSONString(m), DEFAULT_EXPIRE_SECOND, TimeUnit.SECONDS); + } + } else { + m = JSON.parseObject(bucket.get(), clazz); + } + return m; + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param id 数据Id。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @param seconds 过期秒数。 + * @return 数据对象。 + */ + public M getFromCache( + String key, Serializable id, Function f, Class clazz, Integer seconds) { + M m; + RBucket bucket = redissonClient.getBucket(key); + if (!bucket.isExists()) { + m = f.apply(id); + if (m != null) { + if (seconds == null) { + seconds = DEFAULT_EXPIRE_SECOND; + } + bucket.set(JSON.toJSONString(m), seconds, TimeUnit.SECONDS); + } + } else { + m = JSON.parseObject(bucket.get(), clazz); + } + return m; + } + + /** + * 移除指定Key。 + * + * @param key 键名。 + */ + public void evictFormCache(String key) { + redissonClient.getBucket(key).delete(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..1cac49fc --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.redis.config.RedissonConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-satoken/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-satoken/pom.xml new file mode 100644 index 00000000..d2b782dd --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-satoken/pom.xml @@ -0,0 +1,49 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-satoken + 1.0.0 + common-satoken + jar + + 1.37.0 + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + cn.dev33 + sa-token-redis-fastjson + ${sa-token.version} + + + + cn.dev33 + sa-token-alone-redis + ${sa-token.version} + + + + org.apache.commons + commons-pool2 + + + com.orangeforms + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java new file mode 100644 index 00000000..8838858f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.satoken.annotation; + +import java.lang.annotation.*; + +/** + * 所有标记该注解的接口,不能使用SaToken进行权限验证。 + * 必须通过橙单自身的动态验证完成,即基于URL的验证。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SaTokenDenyAuth { +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java new file mode 100644 index 00000000..662bd7e7 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.satoken.listener; + +import com.orangeforms.common.satoken.util.SaTokenUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +/** + * 后台服务启动的时候扫描服务中标有权限字,并同步到Redis,以供接口查询所有使用到的权限字。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class SaTokenPermCodeScanListener implements ApplicationListener { + + @Autowired + private SaTokenUtil saTokenUtil; + + @Override + public void onApplicationEvent(@NonNull ApplicationReadyEvent event) { + saTokenUtil.collectPermCodes(event); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java new file mode 100644 index 00000000..750c3a4a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java @@ -0,0 +1,283 @@ +package com.orangeforms.common.satoken.util; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.LoginUserInfo; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.AopTargetUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth; +import org.redisson.api.RMap; +import org.redisson.api.RSet; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 通用工具方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class SaTokenUtil { + + @Autowired + private RedissonClient redissonClient; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + @Value("${spring.application.name}") + private String applicationName; + + public static final String SA_TOKEN_PERM_CODES_KEY = "SaTokenPermCodes"; + public static final String SA_TOKEN_PERM_CODES_PUBLISH_TOPIC = "SaTokenPermCodesTopic"; + + /** + * 处理免验证接口。目前仅用于微服务的业务服务。 + */ + public void handleNoAuthIntercept() { + if (!StpUtil.isLogin()) { + return; + } + SaSession session = StpUtil.getTokenSession(); + if (session != null) { + TokenData tokenData = JSON.toJavaObject( + (JSONObject) session.get(TokenData.REQUEST_ATTRIBUTE_NAME), TokenData.class); + TokenData.addToRequest(tokenData); + tokenData.setToken(session.getToken()); + } + } + + /** + * 处理权限验证,通常在拦截器中调用。用于微服务中业务服务。 + * + * @param request 当前请求。 + * @param handler 拦截器中的处理器。 + * @return 拦截验证处理结果。 + */ + public ResponseResult handleAuthInterceptEx(HttpServletRequest request, Object handler) { + String appCode = MyCommonUtil.getAppCodeFromRequest(); + if (StrUtil.isNotBlank(appCode)) { + String token = request.getHeader(TokenData.REQUEST_ATTRIBUTE_NAME); + if (StrUtil.isBlank(token)) { + String errorMessage = "第三方登录没有包含Token信息!"; + return ResponseResult.error( + HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage); + } + TokenData tokenData = JSON.parseObject(token, TokenData.class); + TokenData.addToRequest(tokenData); + return ResponseResult.success(); + } + String dontAuth = request.getHeader(ApplicationConstant.HTTP_HEADER_DONT_AUTH); + if (BooleanUtil.toBoolean(dontAuth)) { + this.handleNoAuthIntercept(); + return ResponseResult.success(); + } + return this.handleAuthIntercept(request, handler); + } + + /** + * 处理权限验证,通常在拦截器中调用。通常用于单体服务。 + * + * @param request 当前请求。 + * @param handler 拦截器中的处理器。 + * @return 拦截验证处理结果。 + */ + public ResponseResult handleAuthIntercept(HttpServletRequest request, Object handler) { + if (!(handler instanceof HandlerMethod)) { + return ResponseResult.success(); + } + Method method = ((HandlerMethod) handler).getMethod(); + String errorMessage; + //如果没有登录则直接交给satoken注解去验证。 + if (!StpUtil.isLogin()) { + // 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权 + if (BooleanUtil.isTrue(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class))) { + return ResponseResult.success(); + } + errorMessage = "非免登录接口必须包含Token信息!"; + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage); + } + //对于已经登录的用户一定存在session对象。 + SaSession session = StpUtil.getTokenSession(); + if (session == null) { + errorMessage = "用户会话已过期,请重新登录!"; + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage); + } + TokenData tokenData = JSON.toJavaObject( + (JSONObject) session.get(TokenData.REQUEST_ATTRIBUTE_NAME), TokenData.class); + TokenData.addToRequest(tokenData); + //将最初前端请求使用的token数据赋值给TokenData对象,以便于再次调用其他API接口时直接使用。 + tokenData.setToken(session.getToken()); + //如果是管理员可以直接跳过验证了。 + //基于橙单内部的权限规则优先验证,主要用于内部的白名单接口,以及在线表单和工作流那些动态接口的权限验证。 + if (Boolean.TRUE.equals(tokenData.getIsAdmin()) + || this.hasPermission(tokenData.getSessionId(), request.getRequestURI())) { + return ResponseResult.success(); + } + //对于应由白名单鉴权的接口,都会添加SaTokenDenyAuth注解,因此这里需要判断一下, + //对于此类接口无需SaToken验证了,而是直接返回未授权,因为基于url的鉴权在上面的hasPermission中完成了。 + if (method.getAnnotation(SaTokenDenyAuth.class) != null) { + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + try { + //执行基于stoken的注解鉴权。 + SaStrategy.instance.checkMethodAnnotation.accept(method); + } catch (SaTokenException e) { + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + return ResponseResult.success(); + } + + /** + * 构建satoken的登录Id。 + * + * @return 拼接后的完整登录Id。 + */ + public static String makeLoginId(LoginUserInfo userInfo) { + StringBuilder sb = new StringBuilder(128); + sb.append("SATOKEN_LOGIN:"); + if (userInfo.getTenantId() != null) { + sb.append(userInfo.getTenantId()).append(":"); + } + sb.append(userInfo.getLoginName()).append(":").append(userInfo.getUserId()); + return sb.toString(); + } + + /** + * 获取所有的权限字列表数据。 + * + * @return 所有的权限字列表数据。 + */ + public List getAllPermCodes() { + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + if (!permCodeMap.isExists()) { + return CollUtil.empty(String.class); + } + Set permCodeSet = new TreeSet<>(); + for (RMap.Entry> entry : permCodeMap.entrySet()) { + CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey())); + } + return new LinkedList<>(permCodeSet); + } + + /** + * 获取所有租户运营应用的权限字列表数据。 + * + * @return 所有的权限字列表数据。 + */ + public List getAllTenantPermCodes() { + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + if (!permCodeMap.isExists()) { + return CollUtil.empty(String.class); + } + Set permCodeSet = new TreeSet<>(); + for (RMap.Entry> entry : permCodeMap.entrySet()) { + if (!entry.getKey().equals(ApplicationConstant.TENANT_ADMIN_APP_NAME)) { + CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey())); + } + } + return new LinkedList<>(permCodeSet); + } + + /** + * 获取所有租户管理应用的权限字列表数据。 + * + * @return 所有的权限字列表数据。 + */ + public List getAllTenantAdminPermCodes() { + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + if (!permCodeMap.isExists()) { + return CollUtil.empty(String.class); + } + Set permCodeSet = new TreeSet<>(); + for (RMap.Entry> entry : permCodeMap.entrySet()) { + if (entry.getKey().equals(ApplicationConstant.TENANT_ADMIN_APP_NAME)) { + CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey())); + } + } + return new LinkedList<>(permCodeSet); + } + + /** + * 收集当前服务的SaToken权限字列表,并缓存到Redis,便于统一查询。 + * + * @param event 服务应用的启动事件。 + */ + public void collectPermCodes(ApplicationReadyEvent event) { + redissonClient.getTopic(SA_TOKEN_PERM_CODES_PUBLISH_TOPIC) + .addListener(String.class, (channel, message) -> this.doCollect(event)); + this.doCollect(event); + } + + /** + * 向所有已启动的服务发送权限字同步事件。 + */ + public void publishCollectPermCodes() { + RTopic topic = redissonClient.getTopic(SA_TOKEN_PERM_CODES_PUBLISH_TOPIC); + topic.publish(null); + } + + private void doCollect(ApplicationReadyEvent event) { + Map controllerMap = event.getApplicationContext().getBeansWithAnnotation(RestController.class); + Set permCodes = new HashSet<>(); + for (Map.Entry entry : controllerMap.entrySet()) { + Object targetBean = AopTargetUtil.getTarget(entry.getValue()); + Method[] methods = ReflectUtil.getPublicMethods(targetBean.getClass()); + Arrays.stream(methods) + .map(m -> m.getAnnotation(SaCheckPermission.class)) + .filter(Objects::nonNull) + .forEach(anno -> Collections.addAll(permCodes, anno.value())); + } + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + permCodeMap.put(applicationName, permCodes); + } + + @SuppressWarnings("unchecked") + private boolean hasPermission(String sessionId, String url) { + // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。 + Set localPermSet; + String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL."); + Cache.ValueWrapper wrapper = cache.get(permKey); + if (wrapper == null) { + RSet permSet = redissonClient.getSet(permKey); + localPermSet = permSet.readAll(); + cache.put(permKey, localPermSet); + } else { + localPermSet = (Set) wrapper.get(); + } + return CollUtil.contains(localPermSet, url); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java new file mode 100644 index 00000000..d0339da9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.satoken.util; + +import cn.dev33.satoken.stp.StpInterface; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.RedisKeyUtil; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * 自定义权限加载接口实现类 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + @Autowired + private RedissonClient redissonClient; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + /** + * 返回一个账号所拥有的权限码集合 + */ + @SuppressWarnings("unchecked") + @Override + public List getPermissionList(Object loginId, String loginType) { + TokenData tokenData = TokenData.takeFromRequest(); + String permCodeKey = RedisKeyUtil.makeSessionPermCodeKey(tokenData.getSessionId()); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERM_CODE_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERM_CODE_CACHE can't be NULL"); + Cache.ValueWrapper wrapper = cache.get(permCodeKey); + if (wrapper != null) { + return (List) wrapper.get(); + } + RSet permCodeSet = redissonClient.getSet(permCodeKey); + Set localPermCodeSet = permCodeSet.readAll(); + List permCodeList = new ArrayList<>(localPermCodeSet); + cache.put(permCodeKey, permCodeList); + return permCodeList; + } + + /** + * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) + */ + @Override + public List getRoleList(Object loginId, String loginType) { + return new ArrayList<>(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-sequence/pom.xml new file mode 100644 index 00000000..36502af3 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/pom.xml @@ -0,0 +1,24 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-sequence + 1.0.0 + common-sequence + jar + + + + com.orangeforms + common-core + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java new file mode 100644 index 00000000..327ce435 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java @@ -0,0 +1,14 @@ +package com.orangeforms.common.sequence.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-sequence模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({IdGeneratorProperties.class}) +public class IdGeneratorAutoConfig { + +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java new file mode 100644 index 00000000..f20076d8 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.sequence.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common-sequence模块的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-sequence") +public class IdGeneratorProperties { + + /** + * 基础版生成器所需的WorkNode参数值。仅当advanceIdGenerator为false时生效。 + */ + private Integer snowflakeWorkNode = 1; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java new file mode 100644 index 00000000..fccf75de --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.sequence.generator; + +import cn.hutool.core.lang.Snowflake; + +/** + * 基础版snowflake计算工具类。 + * 和SnowflakeIdGenerator相比,相同点是均为基于Snowflake算法的生成器。不同点在于当前类的 + * WorkNodeId是通过配置文件静态指定的。而SnowflakeIdGenerator的WorkNodeId是由zk生成的。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class BasicIdGenerator implements MyIdGenerator { + + private final Snowflake snowflake; + + /** + * 构造函数。 + * + * @param workNode 工作节点。 + */ + public BasicIdGenerator(Integer workNode) { + snowflake = new Snowflake(workNode, 0); + } + + /** + * 获取基于Snowflake算法的数值型Id。 + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + @Override + public long nextLongId() { + return this.snowflake.nextId(); + } + + /** + * 获取基于Snowflake算法的字符串Id。 + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + @Override + public String nextStringId() { + return this.snowflake.nextIdStr(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java new file mode 100644 index 00000000..209d3c8e --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.sequence.generator; + +/** + * 分布式Id生成器的统一接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface MyIdGenerator { + + /** + * 获取数值型分布式Id。 + * + * @return 生成后的Id。 + */ + long nextLongId(); + + /** + * 获取字符型分布式Id。 + * + * @return 生成后的Id。 + */ + String nextStringId(); +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java new file mode 100644 index 00000000..441ba9d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.sequence.wrapper; + +import com.orangeforms.common.sequence.config.IdGeneratorProperties; +import com.orangeforms.common.sequence.generator.BasicIdGenerator; +import com.orangeforms.common.sequence.generator.MyIdGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +/** + * 分布式Id生成器的封装类。该对象可根据配置选择不同的生成器实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class IdGeneratorWrapper { + + @Autowired + private IdGeneratorProperties properties; + /** + * Id生成器接口对象。 + */ + private MyIdGenerator idGenerator; + + /** + * 今后如果支持更多Id生成器时,可以在该函数内实现不同生成器的动态选择。 + */ + @PostConstruct + public void init() { + idGenerator = new BasicIdGenerator(properties.getSnowflakeWorkNode()); + } + + /** + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + public long nextLongId() { + return idGenerator.nextLongId(); + } + + /** + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + public String nextStringId() { + return idGenerator.nextStringId(); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..f917b714 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.sequence.config.IdGeneratorAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-swagger/pom.xml b/OrangeFormsOpen-MybatisFlex/common/common-swagger/pom.xml new file mode 100644 index 00000000..683c9952 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-swagger/pom.xml @@ -0,0 +1,40 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-swagger + 1.0.0 + common-swagger + jar + + + + + com.github.xiaoymin + knife4j-dependencies + ${knife4j.version} + pom + import + + + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + com.orangeforms + common-core + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java new file mode 100644 index 00000000..1ad2a2ae --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java @@ -0,0 +1,70 @@ +package com.orangeforms.common.swagger.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 自动加载bean的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties(SwaggerProperties.class) +@ConditionalOnProperty(prefix = "common-swagger", name = "enabled") +public class SwaggerAutoConfiguration { + + @Bean + public GroupedOpenApi upmsApi(SwaggerProperties p) { + String[] paths = {"/admin/upms/**"}; + String[] packagedToMatch = {p.getServiceBasePackage() + ".upms.controller"}; + return GroupedOpenApi.builder().group("用户权限分组接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi bizApi(SwaggerProperties p) { + String[] paths = {"/admin/app/**"}; + String[] packagedToMatch = {p.getServiceBasePackage() + ".app.controller"}; + return GroupedOpenApi.builder().group("业务应用分组接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi workflowApi(SwaggerProperties p) { + String[] paths = {"/admin/flow/**"}; + String[] packagedToMatch = {p.getBasePackage() + ".common.flow.controller"}; + return GroupedOpenApi.builder().group("工作流通用操作接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi onlineApi(SwaggerProperties p) { + String[] paths = {"/admin/online/**"}; + String[] packagedToMatch = {p.getBasePackage() + ".common.online.controller"}; + return GroupedOpenApi.builder().group("在线表单操作接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi reportApi(SwaggerProperties p) { + String[] paths = {"/admin/report/**"}; + String[] packagedToMatch = {p.getBasePackage() + ".common.report.controller"}; + return GroupedOpenApi.builder().group("报表打印操作接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public OpenAPI customOpenApi(SwaggerProperties p) { + Info info = new Info().title(p.getTitle()).version(p.getVersion()).description(p.getDescription()); + return new OpenAPI().info(info); + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java new file mode 100644 index 00000000..7f84999f --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java @@ -0,0 +1,45 @@ +package com.orangeforms.common.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 配置参数对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties("common-swagger") +public class SwaggerProperties { + + /** + * 是否开启Swagger。 + */ + private Boolean enabled; + + /** + * Swagger解析的基础包路径。 + **/ + private String basePackage = ""; + + /** + * Swagger解析的服务包路径。 + **/ + private String serviceBasePackage = ""; + + /** + * ApiInfo中的标题。 + **/ + private String title = ""; + + /** + * ApiInfo中的描述信息。 + **/ + private String description = ""; + + /** + * ApiInfo中的版本信息。 + **/ + private String version = ""; +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java new file mode 100644 index 00000000..4bba5b3b --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java @@ -0,0 +1,194 @@ +package com.orangeforms.common.swagger.plugin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.models.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.util.*; +import java.util.stream.Stream; + +/** + * @author xiaoymin@foxmail.com + */ +@Slf4j +@Component +public class MyGlobalOperationCustomer implements GlobalOperationCustomizer { + + /** + * 注解包路径名称 + */ + private static final String REF_KEY = "$ref"; + private static final String REF_SCHEMA_PREFIX = "#/components/schemas/"; + private final Map, Set> cacheClassProperties = MapUtil.newHashMap(); + private static final String EXTENSION_ORANGE_FORM_NAME = "x-orangeforms"; + private static final String EXTENSION_ORANGE_FORM_IGNORE_NAME = "x-orangeforms-ignore-parameters"; + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + this.handleSummary(operation, handlerMethod); + if (handlerMethod.getMethod().getParameterCount() <= 0) { + return operation; + } + Parameter[] parameters = handlerMethod.getMethod().getParameters(); + if (ArrayUtil.isEmpty(parameters)) { + return operation; + } + Map properties = MapUtil.newHashMap(); + Map extensions = MapUtil.newHashMap(); + Set ignoreFieldName = CollUtil.newHashSet(); + List required = new ArrayList<>(); + Map paramMap = getParameterDescription(handlerMethod.getMethod()); + for (Parameter parameter : parameters) { + Annotation[] annos = parameter.getAnnotations(); + if (ArrayUtil.isEmpty(annos)) { + continue; + } + long count = Stream.of(annos).filter(anno -> anno.annotationType().equals(MyRequestBody.class)).count(); + if (count > 0) { + this.handleParameterDetail(parameter, properties, paramMap, ignoreFieldName, required); + } + } + if (!properties.isEmpty()) { + extensions.put("properties", properties); + extensions.put("type", "object"); + //required字段 + if (!required.isEmpty()) { + extensions.put("required", required); + } + String generateSchemaName = handlerMethod.getMethod().getName() + "DynamicReq"; + Map orangeExtensions = MapUtil.newHashMap(); + orangeExtensions.put(generateSchemaName, extensions); + //增加扩展属性 + operation.addExtension(EXTENSION_ORANGE_FORM_NAME, orangeExtensions); + if (!ignoreFieldName.isEmpty()) { + operation.addExtension(EXTENSION_ORANGE_FORM_IGNORE_NAME, ignoreFieldName); + } + } + return operation; + } + + private void handleSummary(Operation operation, HandlerMethod handlerMethod) { + io.swagger.v3.oas.annotations.Operation operationAnno = + handlerMethod.getMethod().getAnnotation(io.swagger.v3.oas.annotations.Operation.class); + if (operationAnno == null || StrUtil.isBlank(operationAnno.summary())) { + operation.setSummary(handlerMethod.getMethod().getName()); + } + } + + private void handleParameterDetail( + Parameter parameter, + Map properties, + Map paramMap, + Set ignoreFieldName, + List required) { + Class parameterType = parameter.getType(); + String schemaName = parameterType.getSimpleName(); + //添加忽律参数名称 + ignoreFieldName.addAll(getClassFields(parameterType)); + //处理schema注解别名的情况 + Schema schema = parameterType.getAnnotation(Schema.class); + if (schema != null && StrUtil.isNotBlank(schema.name())) { + schemaName = schema.name(); + } + Map value = MapUtil.newHashMap(); + //此处需要判断parameter的基础数据类型 + if (parameterType.isPrimitive() || parameterType.getName().startsWith("java.lang")) { + //基础数据类型 + ignoreFieldName.add(parameter.getName()); + value.put("type", parameterType.getSimpleName().toLowerCase()); + //判断format + } else if (Collection.class.isAssignableFrom(parameterType)) { + //集合类型 + value.put("type", "array"); + //获取泛型 + getGenericType(parameterType, parameter.getParameterizedType()) + .ifPresent(s -> value.put("items", MapUtil.builder(REF_KEY, REF_SCHEMA_PREFIX + s).build())); + } else { + //引用类型 + value.put(REF_KEY, REF_SCHEMA_PREFIX + schemaName); + } + //补一个description + io.swagger.v3.oas.annotations.Parameter paramAnnotation = paramMap.get(parameter.getName()); + if (paramAnnotation != null) { + //忽略该参数 + ignoreFieldName.add(paramAnnotation.name()); + value.put("description", paramAnnotation.description()); + if (StrUtil.isNotBlank(paramAnnotation.example())) { + value.put("default", paramAnnotation.example()); + } + // required参数 + if (paramAnnotation.required()) { + required.add(parameter.getName()); + } + } + properties.put(parameter.getName(), value); + } + + private Optional getGenericType(Class clazz, Type type) { + Type genericSuperclass = clazz.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType || type instanceof ParameterizedType) { + if (type instanceof ParameterizedType) { + genericSuperclass = type; + } + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + return Optional.of(((Class) actualTypeArguments[0]).getSimpleName()); + } + return Optional.empty(); + } + + private Set getClassFields(Class parameterType) { + if (parameterType == null) { + return Collections.emptySet(); + } + if (cacheClassProperties.containsKey(parameterType)) { + return cacheClassProperties.get(parameterType); + } + Set fieldNames = new HashSet<>(); + try { + Field[] fields = parameterType.getDeclaredFields(); + if (fields.length > 0) { + for (Field field : fields) { + fieldNames.add(field.getName()); + } + cacheClassProperties.put(parameterType, fieldNames); + return fieldNames; + } + } catch (Exception e) { + //ignore + } + return Collections.emptySet(); + } + + private Map getParameterDescription(Method method) { + Parameters parameters = method.getAnnotation(Parameters.class); + Map resultMap = MapUtil.newHashMap(); + if (parameters != null) { + io.swagger.v3.oas.annotations.Parameter[] parameters1 = parameters.value(); + if (parameters1 != null && parameters1.length > 0) { + for (io.swagger.v3.oas.annotations.Parameter parameter : parameters1) { + resultMap.put(parameter.name(), parameter); + } + return resultMap; + } + } else { + io.swagger.v3.oas.annotations.Parameter parameter = + method.getAnnotation(io.swagger.v3.oas.annotations.Parameter.class); + if (parameter != null) { + resultMap.put(parameter.name(), parameter); + } + } + return resultMap; + } +} diff --git a/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..b94a3251 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.swagger.config.SwaggerAutoConfiguration \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/common/pom.xml b/OrangeFormsOpen-MybatisFlex/common/pom.xml new file mode 100644 index 00000000..9ba52d48 --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/common/pom.xml @@ -0,0 +1,30 @@ + + + + com.orangeforms + OrangeFormsOpen + 1.0.0 + + 4.0.0 + + common + pom + + + common-dbutil + common-ext + common-core + common-log + common-dict + common-datafilter + common-satoken + common-online + common-flow-online + common-flow + common-redis + common-minio + common-sequence + common-swagger + + diff --git a/OrangeFormsOpen-MybatisFlex/pom.xml b/OrangeFormsOpen-MybatisFlex/pom.xml new file mode 100644 index 00000000..d3710c3d --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/pom.xml @@ -0,0 +1,176 @@ + + + 4.0.0 + + com.orangeforms + OrangeFormsOpen + 1.0.0 + OrangeFormsOpen + pom + + + 3.1.6 + 3.1.6 + UTF-8 + 17 + 17 + 17 + OrangeFormsOpen + + 2.10.13 + 20.0 + 2.6 + 4.4 + 1.8 + 5.2.2 + 5.0.0 + 5.8.23 + 0.12.3 + 1.2.83 + 1.1.5 + 2.9.3 + 1.18.20 + 8.0.1.Final + 7.0.1 + 3.15.4 + 8.4.5 + 2.0.0 + 4.5.0 + + 1.2.16 + 1.7.7 + 5.3.3 + + + + application-webadmin + common + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-logging + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-cache + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.security + spring-security-crypto + + + + org.springframework.boot + spring-boot-starter-actuator + + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + mysql + mysql-connector-java + 8.0.22 + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + src/main/resources + + **/*.* + + false + + + src/main/java + + **/*.xml + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + -parameters + + ${maven.compiler.target} + ${maven.compiler.source} + UTF-8 + + + org.projectlombok + lombok + ${lombok.version} + + + com.mybatis-flex + mybatis-flex-processor + ${mybatisflex.version} + + + + + + + diff --git a/OrangeFormsOpen-MybatisFlex/zz-resource/.DS_Store b/OrangeFormsOpen-MybatisFlex/zz-resource/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..97474e58245d4abc0a3ad1f4a8c84ec6d6ee751d GIT binary patch literal 6148 zcmeHKK}rKb5bQP)LJX3lg2H?tU|!%F*B9jAO*Dx{al-~Ym2z z5;x=|BHb`kvoqB*wFNt~Lqt4SPWnV$B5I)sHruFv2pN~M7CY9{0kY09rYCx!IX%;2 zjqM%ofIINl9FT9fO%obX58rI*`^~2P;W(eoRq*cqWgTypr|BrmM;LFtUA>$hAHAQ| zb0=GK=lA9H1E!=7bmq|bNVnK$dUS`qCPRBs(aJp7#4YRnXq-&lqNR%W}Qu&$2&pmgFSqSx+#J5Y6?5g)s9{$JGJ|Eo#<gYJ*qqz^^*+1!^UFX8-^I literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-MybatisFlex/zz-resource/db-scripts/.DS_Store b/OrangeFormsOpen-MybatisFlex/zz-resource/db-scripts/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0\n\n \n \n \n \n \n \n \n Flow_0d86buw\n \n \n \n \n \n Flow_1bxwcza\n \n \n \n \n \n \n \n \n \n \n Flow_0d86buw\n Flow_1u40dt7\n \n \n \n \n \n \n \n \n \n \n Flow_1u40dt7\n Flow_05s1j0n\n \n \n \n \n \n \n \n \n \n \n Flow_05s1j0n\n Flow_1bxwcza\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n', 0, 0, 1809132177523216384, 1809132635633487872, NULL, '{\"middle\":\"DD\",\"idWidth\":5,\"prefix\":\"LL\",\"precisionTo\":\"DAYS\",\"calculateWhenView\":true}', '{\"approvalStatusDict\":[{\"id\":1,\"name\":\"同意\",\"_X_ROW_KEY\":\"row_57\"},{\"id\":2,\"name\":\"拒绝\",\"_X_ROW_KEY\":\"row_58\"},{\"id\":3,\"name\":\"驳回\",\"_X_ROW_KEY\":\"row_59\"},{\"id\":4,\"name\":\"会签同意\",\"_X_ROW_KEY\":\"row_60\"},{\"id\":5,\"name\":\"会签拒绝\",\"_X_ROW_KEY\":\"row_61\"}],\"notifyTypes\":[\"email\"],\"cascadeDeleteBusinessData\":true,\"supportRevive\":false}', '2024-07-05 16:36:39', 1808020007993479168, '2024-07-05 16:35:15', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_entry_publish +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_entry_publish`; +CREATE TABLE `zz_flow_entry_publish` ( + `entry_publish_id` bigint NOT NULL COMMENT '主键Id', + `entry_id` bigint NOT NULL COMMENT '流程Id', + `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的定义Id', + `deploy_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的部署Id', + `publish_version` int NOT NULL COMMENT '发布版本', + `active_status` bit(1) NOT NULL COMMENT '激活状态', + `main_version` bit(1) NOT NULL COMMENT '是否为主版本', + `extension_data` varchar(3000) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程的自定义扩展数据', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `publish_time` datetime NOT NULL COMMENT '发布时间', + `init_task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '第一个非开始节点任务的附加信息', + `analyzed_node_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '分析后的节点JSON信息', + PRIMARY KEY (`entry_publish_id`) USING BTREE, + UNIQUE KEY `uk_process_definition_id` (`process_definition_id`) USING BTREE, + KEY `idx_entry_id` (`entry_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程发布表'; + +-- ---------------------------- +-- Records of zz_flow_entry_publish +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_entry_publish` VALUES (1809144428770627584, 1809143991627681792, 'flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'bcd05b06-3aa9-11ef-86ec-acde48001122', 1, b'1', b'1', '{\"approvalStatusDict\":[{\"id\":1,\"name\":\"同意\",\"_X_ROW_KEY\":\"row_57\"},{\"id\":2,\"name\":\"拒绝\",\"_X_ROW_KEY\":\"row_58\"},{\"id\":3,\"name\":\"驳回\",\"_X_ROW_KEY\":\"row_59\"},{\"id\":4,\"name\":\"会签同意\",\"_X_ROW_KEY\":\"row_60\"},{\"id\":5,\"name\":\"会签拒绝\",\"_X_ROW_KEY\":\"row_61\"}],\"notifyTypes\":[\"email\"],\"cascadeDeleteBusinessData\":true,\"supportRevive\":false}', 1808020007993479168, '2024-07-05 16:36:59', '{\"assignee\":\"${startUserName}\",\"formId\":1809132635633487872,\"groupType\":\"ASSIGNEE\",\"operationList\":[{\"showOrder\":\"0\",\"id\":\"1720168540672\",\"label\":\"同意\",\"type\":\"agree\"}],\"readOnly\":false,\"taskKey\":\"Activity_0vjtv0p\",\"taskType\":1,\"variableList\":[]}', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_entry_publish_variable +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_entry_publish_variable`; +CREATE TABLE `zz_flow_entry_publish_variable` ( + `variable_id` bigint NOT NULL COMMENT '主键Id', + `entry_publish_id` bigint NOT NULL COMMENT '流程Id', + `variable_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '变量名', + `show_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '显示名', + `variable_type` int NOT NULL COMMENT '变量类型', + `bind_datasource_id` bigint DEFAULT NULL COMMENT '绑定数据源Id', + `bind_relation_id` bigint DEFAULT NULL COMMENT '绑定数据源关联Id', + `bind_column_id` bigint DEFAULT NULL COMMENT '绑定字段Id', + `builtin` bit(1) NOT NULL COMMENT '是否内置', + PRIMARY KEY (`variable_id`) USING BTREE, + KEY `idx_entry_publish_id` (`entry_publish_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程发布变量表'; + +-- ---------------------------- +-- Records of zz_flow_entry_publish_variable +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_entry_publish_variable` VALUES (1809144430116999168, 1809144428770627584, 'operationType', '审批类型', 1, NULL, NULL, NULL, b'1'); +INSERT INTO `zz_flow_entry_publish_variable` VALUES (1809144430116999169, 1809144428770627584, 'startUserName', '流程启动用户', 0, NULL, NULL, NULL, b'1'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_entry_variable +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_entry_variable`; +CREATE TABLE `zz_flow_entry_variable` ( + `variable_id` bigint NOT NULL COMMENT '主键Id', + `entry_id` bigint NOT NULL COMMENT '流程Id', + `variable_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '变量名', + `show_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '显示名', + `variable_type` int NOT NULL COMMENT '变量类型', + `bind_datasource_id` bigint DEFAULT NULL COMMENT '绑定数据源Id', + `bind_relation_id` bigint DEFAULT NULL COMMENT '绑定数据源关联Id', + `bind_column_id` bigint DEFAULT NULL COMMENT '绑定字段Id', + `builtin` bit(1) NOT NULL COMMENT '是否内置', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`variable_id`) USING BTREE, + UNIQUE KEY `uk_entry_id_variable_name` (`entry_id`,`variable_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程变量表'; + +-- ---------------------------- +-- Records of zz_flow_entry_variable +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_entry_variable` VALUES (1809143992151969793, 1809143991627681792, 'operationType', '审批类型', 1, NULL, NULL, NULL, b'1', '2024-07-05 16:35:15'); +INSERT INTO `zz_flow_entry_variable` VALUES (1809143992630120448, 1809143991627681792, 'startUserName', '流程启动用户', 0, NULL, NULL, NULL, b'1', '2024-07-05 16:35:15'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_message +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_message`; +CREATE TABLE `zz_flow_message` ( + `message_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用Id', + `message_type` tinyint NOT NULL COMMENT '消息类型', + `message_content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '消息内容', + `remind_count` int DEFAULT '0' COMMENT '催办次数', + `work_order_id` bigint DEFAULT NULL COMMENT '工单Id', + `process_definition_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义Id', + `process_definition_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义标识', + `process_definition_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义名称', + `process_instance_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程实例Id', + `process_instance_initiator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程实例发起者', + `task_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程任务Id', + `task_definition_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程任务定义标识', + `task_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程任务名称', + `task_start_time` datetime DEFAULT NULL COMMENT '任务开始时间', + `task_finished` bit(1) NOT NULL DEFAULT b'0' COMMENT '任务是否已完成', + `task_assignee` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务指派人登录名', + `business_data_shot` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '业务数据快照', + `online_form_data` bit(1) DEFAULT NULL COMMENT '是否为在线表单消息数据', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者显示名', + PRIMARY KEY (`message_id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE, + KEY `idx_notified_username` (`task_assignee`) USING BTREE, + KEY `idx_process_instance_id` (`process_instance_id`) USING BTREE, + KEY `idx_message_type` (`message_type`) USING BTREE, + KEY `idx_task_id` (`task_id`) USING BTREE, + KEY `idx_task_finished` (`task_finished`) USING BTREE, + KEY `idx_update_time` (`update_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程消息通知表'; + +-- ---------------------------- +-- Table structure for zz_flow_msg_candidate_identity +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_msg_candidate_identity`; +CREATE TABLE `zz_flow_msg_candidate_identity` ( + `id` bigint NOT NULL COMMENT '主键Id', + `message_id` bigint NOT NULL COMMENT '流程任务Id', + `candidate_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '候选身份类型', + `candidate_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '候选身份Id', + PRIMARY KEY (`id`), + KEY `idx_candidate_id` (`candidate_id`) USING BTREE, + KEY `idx_message_id` (`message_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程消息通知候选人表'; + +-- ---------------------------- +-- Table structure for zz_flow_msg_identity_operation +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_msg_identity_operation`; +CREATE TABLE `zz_flow_msg_identity_operation` ( + `id` bigint NOT NULL COMMENT '主键Id', + `message_id` bigint NOT NULL COMMENT '流程任务Id', + `login_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户登录名', + `operation_type` int NOT NULL COMMENT '操作类型', + `operation_time` datetime NOT NULL COMMENT '操作时间', + PRIMARY KEY (`id`), + KEY `idx_message_id` (`message_id`) USING BTREE, + KEY `idx_login_name` (`login_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程消息候选人操作表'; + +-- ---------------------------- +-- Table structure for zz_flow_multi_instance_trans +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_multi_instance_trans`; +CREATE TABLE `zz_flow_multi_instance_trans` ( + `id` bigint NOT NULL COMMENT '主键Id', + `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程实例Id', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务Id', + `task_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务标识', + `multi_instance_exec_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '会签任务的执行Id', + `execution_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务的执行Id', + `assignee_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '会签指派人列表', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_login_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者登录名', + `create_username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者用户名', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_execution_id_task_id` (`execution_id`,`task_id`) USING BTREE, + KEY `idx_multi_instance_exec_id` (`multi_instance_exec_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程多实例任务审批流水表'; + +-- ---------------------------- +-- Table structure for zz_flow_task_comment +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_task_comment`; +CREATE TABLE `zz_flow_task_comment` ( + `id` bigint NOT NULL COMMENT '主键Id', + `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程实例Id', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务Id', + `task_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务标识', + `task_name` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务名称', + `target_task_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '目标任务标识', + `execution_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务的执行Id', + `multi_instance_exec_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '会签任务的执行Id', + `approval_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '审批类型', + `task_comment` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '批注内容', + `delegate_assignee` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '委托指定人,比如加签、转办等', + `custom_business_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '自定义数据。开发者可自行扩展,推荐使用JSON格式数据', + `head_image_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '审批用户头像', + `create_user_id` bigint DEFAULT NULL COMMENT '创建者Id', + `create_login_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建者登录名', + `create_username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建者用户名', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_multi_instance_exec_id` (`multi_instance_exec_id`) USING BTREE, + KEY `idx_process_instance_id` (`process_instance_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程任务审批表'; + +-- ---------------------------- +-- Records of zz_flow_task_comment +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_task_comment` VALUES (1809146487481831424, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'e200f745-3aaa-11ef-86ec-acde48001122', 'Activity_0vjtv0p', '录入', NULL, 'e1fbee31-3aaa-11ef-86ec-acde48001122', NULL, 'agree', NULL, NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:45:10'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146598064656384, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'e322e20d-3aaa-11ef-86ec-acde48001122', 'Activity_06g14pf', '审批A', NULL, 'e1fbee31-3aaa-11ef-86ec-acde48001122', NULL, 'agree', '11', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:45:36'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146699361292288, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'f2dcc311-3aaa-11ef-86ec-acde48001122', 'Activity_0dn7u52', '审批B', NULL, NULL, NULL, 'reject', '11', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:46:00'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146743762194432, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'ffef78e6-3aaa-11ef-86ec-acde48001122', 'Activity_06g14pf', '审批A', NULL, NULL, NULL, 'reject', '33', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:46:11'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146774330281984, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', '0669cc2a-3aab-11ef-86ec-acde48001122', 'Activity_0vjtv0p', '录入', NULL, '0669cc28-3aab-11ef-86ec-acde48001122', NULL, 'agree', '44', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:46:18'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_task_ext +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_task_ext`; +CREATE TABLE `zz_flow_task_ext` ( + `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的定义Id', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎任务Id', + `operation_list_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '操作列表JSON', + `variable_list_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '变量列表JSON', + `assignee_list_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '存储多实例的assigneeList的JSON', + `group_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '分组类型', + `dept_post_list_json` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存岗位相关的数据', + `role_ids` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存角色Id数据', + `dept_ids` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存部门Id数据', + `candidate_usernames` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存候选组用户名数据', + `copy_list_json` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '抄送相关的数据', + `extra_data_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '用户任务的扩展属性,存储为JSON的字符串格式', + PRIMARY KEY (`process_definition_id`,`task_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程流程图任务扩展表'; + +-- ---------------------------- +-- Records of zz_flow_task_ext +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_task_ext` VALUES ('flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'Activity_06g14pf', '[{\"showOrder\":\"0\",\"id\":\"1720168555059\",\"label\":\"同意\",\"type\":\"agree\"},{\"showOrder\":\"0\",\"id\":\"1720168558485\",\"label\":\"驳回到起点\",\"type\":\"rejectToStart\"}]', NULL, NULL, 'ASSIGNEE', '[]', NULL, NULL, NULL, '[]', '{\"flowNotifyTypeList\":[\"email\"]}'); +INSERT INTO `zz_flow_task_ext` VALUES ('flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'Activity_0dn7u52', '[{\"showOrder\":\"0\",\"id\":\"1720168573903\",\"label\":\"同意\",\"type\":\"agree\"},{\"showOrder\":\"0\",\"id\":\"1720168577495\",\"label\":\"驳回\",\"type\":\"reject\"}]', NULL, NULL, 'ASSIGNEE', '[]', NULL, NULL, NULL, '[]', '{\"flowNotifyTypeList\":[\"email\"]}'); +INSERT INTO `zz_flow_task_ext` VALUES ('flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'Activity_0vjtv0p', '[{\"showOrder\":\"0\",\"id\":\"1720168540672\",\"label\":\"同意\",\"type\":\"agree\"}]', NULL, NULL, 'ASSIGNEE', '[]', NULL, NULL, NULL, '[]', '{\"flowNotifyTypeList\":[\"email\"]}'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_work_order +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_work_order`; +CREATE TABLE `zz_flow_work_order` ( + `work_order_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `work_order_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '工单编码字段', + `process_definition_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义标识', + `process_definition_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '流程名称', + `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的定义Id', + `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程实例Id', + `online_table_id` bigint DEFAULT NULL COMMENT '在线表单的主表Id', + `table_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用于静态表单的表名', + `business_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '业务主键值', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '未完成的任务Id', + `task_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '未完成的任务名称', + `task_definition_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '未完成的任务标识', + `latest_approval_status` int DEFAULT NULL COMMENT '最近的审批状态', + `flow_status` int NOT NULL DEFAULT '0' COMMENT '流程状态', + `submit_username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '提交用户登录名称', + `dept_id` bigint NOT NULL COMMENT '提交用户所在部门Id', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`work_order_id`) USING BTREE, + UNIQUE KEY `uk_process_instance_id` (`process_instance_id`) USING BTREE, + UNIQUE KEY `uk_work_order_code` (`work_order_code`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE, + KEY `idx_process_definition_key` (`process_definition_key`) USING BTREE, + KEY `idx_create_user_id` (`create_user_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE, + KEY `idx_dept_id` (`dept_id`) USING BTREE, + KEY `idx_table_name` (`table_name`) USING BTREE, + KEY `idx_business_key` (`business_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程工单表'; + +-- ---------------------------- +-- Records of zz_flow_work_order +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_work_order` VALUES (1809146486244511744, NULL, NULL, 'LL20240705DD00001', 'flowLeave', '请假申请', 'flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 1809132251556876288, NULL, '1809146480452177920', NULL, NULL, NULL, NULL, 1, 'admin', 1808020008341606402, '2024-07-05 16:46:18', 1808020007993479168, '2024-07-05 16:45:09', 1808020007993479168, 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_work_order_ext +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_work_order_ext`; +CREATE TABLE `zz_flow_work_order_ext` ( + `id` bigint NOT NULL COMMENT '主键Id', + `work_order_id` bigint NOT NULL COMMENT '工单Id', + `draft_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '草稿数据', + `business_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '业务数据', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_work_order_id` (`work_order_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程工单扩展表'; + +-- ---------------------------- +-- Table structure for zz_global_dict +-- ---------------------------- +DROP TABLE IF EXISTS `zz_global_dict`; +CREATE TABLE `zz_global_dict` ( + `dict_id` bigint NOT NULL COMMENT '主键Id', + `dict_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典编码', + `dict_name` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典中文名称', + `create_user_id` bigint NOT NULL COMMENT '创建用户Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新用户名', + `update_time` datetime NOT NULL COMMENT '更新时间', + `deleted_flag` int NOT NULL COMMENT '逻辑删除字段', + PRIMARY KEY (`dict_id`), + KEY `idx_dict_code` (`dict_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='全局字典表'; + +-- ---------------------------- +-- Table structure for zz_global_dict_item +-- ---------------------------- +DROP TABLE IF EXISTS `zz_global_dict_item`; +CREATE TABLE `zz_global_dict_item` ( + `id` bigint NOT NULL COMMENT '主键Id', + `dict_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典编码', + `item_id` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '字典数据项Id', + `item_name` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典数据项名称', + `show_order` int NOT NULL COMMENT '显示顺序', + `status` int NOT NULL COMMENT '字典状态', + `create_user_id` bigint NOT NULL COMMENT '创建用户Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新用户名', + `update_time` datetime NOT NULL COMMENT '更新时间', + `deleted_flag` int NOT NULL COMMENT '逻辑删除字段', + PRIMARY KEY (`id`), + KEY `idx_show_order` (`show_order`) USING BTREE, + KEY `idx_dict_code_item_id` (`dict_code`,`item_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='全局字典项目表'; + +-- ---------------------------- +-- Table structure for zz_online_column +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_column`; +CREATE TABLE `zz_online_column` ( + `column_id` bigint NOT NULL COMMENT '主键Id', + `column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字段名', + `table_id` bigint NOT NULL COMMENT '数据表Id', + `column_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '数据表中的字段类型', + `full_column_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '数据表中的完整字段类型(包括了精度和刻度)', + `primary_key` bit(1) NOT NULL COMMENT '是否为主键', + `auto_incr` bit(1) NOT NULL COMMENT '是否是自增主键(0: 不是 1: 是)', + `nullable` bit(1) NOT NULL COMMENT '是否可以为空 (0: 不可以为空 1: 可以为空)', + `column_default` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '缺省值', + `column_show_order` int NOT NULL COMMENT '字段在数据表中的显示位置', + `column_comment` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '数据表中的字段注释', + `object_field_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '对象映射字段名称', + `object_field_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '对象映射字段类型', + `numeric_precision` int DEFAULT NULL COMMENT '数值型字段的精度', + `numeric_scale` int DEFAULT NULL COMMENT '数值型字段的刻度', + `filter_type` int NOT NULL DEFAULT '1' COMMENT '字段过滤类型', + `parent_key` bit(1) NOT NULL COMMENT '是否是主键的父Id', + `dept_filter` bit(1) NOT NULL COMMENT '是否部门过滤字段', + `user_filter` bit(1) NOT NULL COMMENT '是否用户过滤字段', + `field_kind` int DEFAULT NULL COMMENT '字段类别', + `max_file_count` int DEFAULT NULL COMMENT '包含的文件文件数量,0表示无限制', + `upload_file_system_type` int DEFAULT '0' COMMENT '上传文件系统类型', + `encoded_rule` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '编码规则的JSON格式数据', + `mask_field_type` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '脱敏字段类型', + `dict_id` bigint DEFAULT NULL COMMENT '字典Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`column_id`), + KEY `idx_table_id` (`table_id`) USING BTREE, + KEY `idx_dict_id` (`dict_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字段表'; + +-- ---------------------------- +-- Records of zz_online_column +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_column` VALUES (1809132252005666816, 'id', 1809132251556876288, 'bigint', 'bigint', b'1', b'0', b'0', NULL, 1, '主键Id', 'id', 'Long', 19, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:48:36', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132252425097216, 'user_id', 1809132251556876288, 'bigint', 'bigint', b'0', b'0', b'0', NULL, 2, '请假用户', 'userId', 'Long', 19, NULL, 0, b'0', b'0', b'0', 21, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:48:47', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132252852916224, 'leave_reason', 1809132251556876288, 'varchar', 'varchar(512)', b'0', b'0', b'0', NULL, 3, '请假原因', 'leaveReason', 'String', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:47', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132253377204224, 'leave_type', 1809132251556876288, 'int', 'int', b'0', b'0', b'0', NULL, 4, '请假类型', 'leaveType', 'Integer', 10, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:44', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132253733720064, 'leave_begin_time', 1809132251556876288, 'datetime', 'datetime', b'0', b'0', b'0', NULL, 5, '开始时间', 'leaveBeginTime', 'Date', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:50', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132254102818816, 'leave_end_time', 1809132251556876288, 'datetime', 'datetime', b'0', b'0', b'0', NULL, 6, '结束时间', 'leaveEndTime', 'Date', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:54', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132254388031488, 'apply_time', 1809132251556876288, 'datetime', 'datetime', b'0', b'0', b'0', NULL, 7, '申请时间', 'applyTime', 'Date', NULL, NULL, 0, b'0', b'0', b'0', 20, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:57', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132254782296064, 'approval_status', 1809132251556876288, 'int', 'int', b'0', b'0', b'1', NULL, 8, '最后审批状态', 'approvalStatus', 'Integer', 10, NULL, 0, b'0', b'0', b'0', 26, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:59', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132255327555584, 'flow_status', 1809132251556876288, 'int', 'int', b'0', b'0', b'1', NULL, 9, '流程状态', 'flowStatus', 'Integer', 10, NULL, 0, b'0', b'0', b'0', 25, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:49:44', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132255679877120, 'username', 1809132251556876288, 'varchar', 'varchar(255)', b'0', b'0', b'1', NULL, 10, '用户名', 'username', 'String', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:49:49', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_column_rule +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_column_rule`; +CREATE TABLE `zz_online_column_rule` ( + `column_id` bigint NOT NULL COMMENT '字段Id', + `rule_id` bigint NOT NULL COMMENT '规则Id', + `prop_data_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '规则属性数据', + PRIMARY KEY (`column_id`,`rule_id`) USING BTREE, + KEY `idx_rule_id` (`rule_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字段和字段规则关联中间表'; + +-- ---------------------------- +-- Table structure for zz_online_datasource +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_datasource`; +CREATE TABLE `zz_online_datasource` ( + `datasource_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `datasource_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据源名称', + `variable_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据源变量名', + `dblink_id` bigint NOT NULL COMMENT '数据库链接Id', + `master_table_id` bigint NOT NULL COMMENT '主表Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`datasource_id`), + UNIQUE KEY `uk_app_code_variable_name` (`app_code`,`variable_name`) USING BTREE, + KEY `idx_master_table_id` (`master_table_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据源表'; + +-- ---------------------------- +-- Records of zz_online_datasource +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_datasource` VALUES (1809132255981867008, NULL, '请假申请', 'dsLeave', 1809055300360081408, 1809132251556876288, '2024-07-05 15:48:37', 1808020007993479168, '2024-07-05 15:48:37', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_datasource_relation +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_datasource_relation`; +CREATE TABLE `zz_online_datasource_relation` ( + `relation_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `relation_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '关联名称', + `variable_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '变量名', + `datasource_id` bigint NOT NULL COMMENT '主数据源Id', + `relation_type` int NOT NULL COMMENT '关联类型', + `master_column_id` bigint NOT NULL COMMENT '主表关联字段Id', + `slave_table_id` bigint NOT NULL COMMENT '从表Id', + `slave_column_id` bigint NOT NULL COMMENT '从表关联字段Id', + `cascade_delete` bit(1) NOT NULL COMMENT '删除主表的时候是否级联删除一对一和一对多的从表数据,多对多只是删除关联,不受到这个标记的影响。', + `left_join` bit(1) NOT NULL COMMENT '是否左连接', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`relation_id`) USING BTREE, + UNIQUE KEY `uk_datasource_id_variable_name` (`datasource_id`,`variable_name`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据源关联表'; + +-- ---------------------------- +-- Table structure for zz_online_datasource_table +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_datasource_table`; +CREATE TABLE `zz_online_datasource_table` ( + `id` bigint NOT NULL COMMENT '主键Id', + `datasource_id` bigint NOT NULL COMMENT '数据源Id', + `relation_id` bigint DEFAULT NULL COMMENT '数据源关联Id', + `table_id` bigint NOT NULL COMMENT '数据表Id', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_relation_id` (`relation_id`) USING BTREE, + KEY `idx_datasource_id` (`datasource_id`) USING BTREE, + KEY `idx_table_id` (`table_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据源和数据表关联的中间表'; + +-- ---------------------------- +-- Records of zz_online_datasource_table +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_datasource_table` VALUES (1809132256292245504, 1809132255981867008, NULL, 1809132251556876288); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_dblink +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_dblink`; +CREATE TABLE `zz_online_dblink` ( + `dblink_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `dblink_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '链接中文名称', + `dblink_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接描述', + `dblink_type` int NOT NULL COMMENT '数据源类型', + `configuration` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '配置信息', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`dblink_id`), + KEY `idx_dblink_type` (`dblink_type`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据库链接表'; + +-- ---------------------------- +-- Records of zz_online_dblink +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_dblink` VALUES (1809055300360081408, NULL, 'mysql-test', NULL, 0, '{\"sid\":true,\"initialPoolSize\":5,\"minPoolSize\":5,\"maxPoolSize\":50,\"host\":\"localhost\",\"port\":3306,\"database\":\"zzdemo-online-open\",\"username\":\"root\",\"password\":\"123456\"}', '2024-07-05 10:42:49', 1809038124504846336, '2024-07-05 10:42:49', 1809038124504846336); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_dict +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_dict`; +CREATE TABLE `zz_online_dict` ( + `dict_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `dict_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典名称', + `dict_type` int NOT NULL COMMENT '字典类型', + `dblink_id` bigint DEFAULT NULL COMMENT '数据库链接Id', + `table_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典表名称', + `dict_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '全局字典编码', + `key_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典表键字段名称', + `parent_key_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典表父键字段名称', + `value_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典值字段名称', + `deleted_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '逻辑删除字段', + `user_filter_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户过滤滤字段名称', + `dept_filter_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '部门过滤滤字段名称', + `tenant_filter_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '租户过滤字段名称', + `tree_flag` bit(1) NOT NULL COMMENT '是否树形标记', + `dict_list_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '获取字典列表数据的url', + `dict_ids_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '根据主键id批量获取字典数据的url', + `dict_data_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '字典的JSON数据', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`dict_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字典表'; + +-- ---------------------------- +-- Table structure for zz_online_form +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_form`; +CREATE TABLE `zz_online_form` ( + `form_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `page_id` bigint NOT NULL COMMENT '页面id', + `form_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '表单编码', + `form_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '表单名称', + `form_kind` int NOT NULL COMMENT '表单类别', + `form_type` int NOT NULL COMMENT '表单类型', + `master_table_id` bigint NOT NULL COMMENT '表单主表id', + `widget_json` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '表单组件JSON', + `params_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '表单参数JSON', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`form_id`) USING BTREE, + UNIQUE KEY `uk_page_id_form_code` (`page_id`,`form_code`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单表单表'; + +-- ---------------------------- +-- Records of zz_online_form +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_form` VALUES (1809132635633487872, NULL, NULL, 1809132177523216384, 'formFlowLeave', '请假申请', 5, 10, 1809132251556876288, '{\"pc\":{\"gutter\":20,\"labelWidth\":100,\"labelPosition\":\"right\",\"operationList\":[],\"customFieldList\":[],\"widgetList\":[{\"widgetType\":3,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132253377204224\",\"dataType\":0},\"showName\":\"请假类型\",\"variableName\":\"leaveType\",\"props\":{\"span\":24,\"placeholder\":\"\",\"step\":1,\"controls\":true,\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{}},{\"widgetType\":1,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132252852916224\",\"dataType\":0},\"showName\":\"请假原因\",\"variableName\":\"leaveReason\",\"props\":{\"span\":24,\"type\":\"text\",\"placeholder\":\"\",\"show-password\":false,\"show-word-limit\":false,\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{}},{\"widgetType\":20,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132253733720064\",\"dataType\":0},\"showName\":\"开始时间\",\"variableName\":\"leaveBeginTime\",\"props\":{\"span\":12,\"placeholder\":\"\",\"type\":\"date\",\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{},\"supportOperation\":false},{\"widgetType\":20,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132254102818816\",\"dataType\":0},\"showName\":\"结束时间\",\"variableName\":\"leaveEndTime\",\"props\":{\"span\":12,\"placeholder\":\"\",\"type\":\"date\",\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{},\"supportOperation\":false}],\"formEventList\":[],\"maskFieldList\":[],\"width\":800,\"fullscreen\":true}}', NULL, '2024-07-05 15:50:07', 1808020007993479168, '2024-07-05 16:34:21', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_form_datasource +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_form_datasource`; +CREATE TABLE `zz_online_form_datasource` ( + `id` bigint NOT NULL COMMENT '主键Id', + `form_id` bigint NOT NULL COMMENT '表单Id', + `datasource_id` bigint NOT NULL COMMENT '数据源Id', + PRIMARY KEY (`id`), + KEY `idx_form_id` (`form_id`) USING BTREE, + KEY `idx_datasource_id` (`datasource_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单表单和数据源关联中间表'; + +-- ---------------------------- +-- Records of zz_online_form_datasource +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_form_datasource` VALUES (1809143766578106368, 1809132635633487872, 1809132255981867008); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_page +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_page`; +CREATE TABLE `zz_online_page` ( + `page_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `page_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '页面编码', + `page_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '页面名称', + `page_type` int NOT NULL COMMENT '页面类型', + `status` int NOT NULL COMMENT '页面编辑状态', + `published` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否发布', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`page_id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE, + KEY `idx_page_code` (`page_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单页面表'; + +-- ---------------------------- +-- Records of zz_online_page +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_page` VALUES (1809132177523216384, NULL, NULL, 'flowLeave', '请假申请', 10, 2, b'1', '2024-07-05 15:48:18', 1808020007993479168, '2024-07-05 16:34:27', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_page_datasource +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_page_datasource`; +CREATE TABLE `zz_online_page_datasource` ( + `id` bigint NOT NULL COMMENT '主键Id', + `page_id` bigint NOT NULL COMMENT '页面主键Id', + `datasource_id` bigint NOT NULL COMMENT '数据源主键Id', + PRIMARY KEY (`id`), + KEY `idx_page_id` (`page_id`) USING BTREE, + KEY `idx_datasource_id` (`datasource_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单页面和数据源关联中间表'; + +-- ---------------------------- +-- Records of zz_online_page_datasource +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_page_datasource` VALUES (1809132256564875264, 1809132177523216384, 1809132255981867008); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_rule +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_rule`; +CREATE TABLE `zz_online_rule` ( + `rule_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `rule_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '规则名称', + `rule_type` int NOT NULL COMMENT '规则类型', + `builtin` bit(1) NOT NULL COMMENT '内置规则标记', + `pattern` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '自定义规则的正则表达式', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + `deleted_flag` int NOT NULL COMMENT '逻辑删除标记', + PRIMARY KEY (`rule_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字段规则表'; + +-- ---------------------------- +-- Records of zz_online_rule +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_rule` VALUES (1, NULL, '只允许整数', 1, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (2, NULL, '只允许数字', 2, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (3, NULL, '只允许英文字符', 3, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (4, NULL, '范围验证', 4, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (5, NULL, '邮箱格式验证', 5, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (6, NULL, '手机格式验证', 6, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_table +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_table`; +CREATE TABLE `zz_online_table` ( + `table_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `table_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '表名称', + `model_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '实体名称', + `dblink_id` bigint NOT NULL COMMENT '数据库链接Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`table_id`), + KEY `idx_dblink_id` (`dblink_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据表'; + +-- ---------------------------- +-- Records of zz_online_table +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_table` VALUES (1809132251556876288, NULL, 'zz_test_flow_leave', 'ZzTestFlowLeave', 1809055300360081408, '2024-07-05 15:48:35', 1808020007993479168, '2024-07-05 15:48:35', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_virtual_column +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_virtual_column`; +CREATE TABLE `zz_online_virtual_column` ( + `virtual_column_id` bigint NOT NULL COMMENT '主键Id', + `table_id` bigint NOT NULL COMMENT '所在表Id', + `object_field_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字段名称', + `object_field_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '属性类型', + `column_prompt` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字段提示名', + `virtual_type` int NOT NULL COMMENT '虚拟字段类型(0: 聚合)', + `datasource_id` bigint NOT NULL COMMENT '关联数据源Id', + `relation_id` bigint DEFAULT NULL COMMENT '关联Id', + `aggregation_table_id` bigint DEFAULT NULL COMMENT '聚合字段所在关联表Id', + `aggregation_column_id` bigint DEFAULT NULL COMMENT '关联表聚合字段Id', + `aggregation_type` int DEFAULT NULL COMMENT '聚合类型(0: sum 1: count 2: avg 3: min 4: max)', + `where_clause_json` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '存储过滤条件的json', + PRIMARY KEY (`virtual_column_id`) USING BTREE, + KEY `idx_database_id` (`datasource_id`) USING BTREE, + KEY `idx_relation_id` (`relation_id`) USING BTREE, + KEY `idx_table_id` (`table_id`) USING BTREE, + KEY `idx_aggregation_column_id` (`aggregation_column_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单虚拟字段表'; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm`; +CREATE TABLE `zz_sys_data_perm` ( + `data_perm_id` bigint NOT NULL COMMENT '主键', + `data_perm_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '显示名称', + `rule_type` tinyint NOT NULL COMMENT '数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`data_perm_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限表'; + +-- ---------------------------- +-- Records of zz_sys_data_perm +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_data_perm` VALUES (1809037881759502336, '查看全部', 0, 1808020007993479168, '2024-07-05 09:33:36', 1808020007993479168, '2024-07-05 09:33:36'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm_dept +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm_dept`; +CREATE TABLE `zz_sys_data_perm_dept` ( + `data_perm_id` bigint NOT NULL COMMENT '数据权限Id', + `dept_id` bigint NOT NULL COMMENT '部门Id', + PRIMARY KEY (`data_perm_id`,`dept_id`), + KEY `idx_dept_id` (`dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限和部门关联表'; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm_menu +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm_menu`; +CREATE TABLE `zz_sys_data_perm_menu` ( + `data_perm_id` bigint NOT NULL COMMENT '数据权限Id', + `menu_id` bigint NOT NULL COMMENT '菜单Id', + PRIMARY KEY (`data_perm_id`,`menu_id`), + KEY `idx_menu_id` (`menu_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限和菜单关联表'; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm_user +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm_user`; +CREATE TABLE `zz_sys_data_perm_user` ( + `data_perm_id` bigint NOT NULL COMMENT '数据权限Id', + `user_id` bigint NOT NULL COMMENT '用户Id', + PRIMARY KEY (`data_perm_id`,`user_id`), + KEY `idx_user_id` (`user_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限和用户关联表'; + +-- ---------------------------- +-- Records of zz_sys_data_perm_user +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_data_perm_user` VALUES (1809037881759502336, 1809038124504846336); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_dept +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_dept`; +CREATE TABLE `zz_sys_dept` ( + `dept_id` bigint NOT NULL COMMENT '部门Id', + `parent_id` bigint DEFAULT NULL COMMENT '父部门Id', + `dept_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门名称', + `show_order` int NOT NULL COMMENT '兄弟部分之间的显示顺序,数字越小越靠前', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`dept_id`) USING BTREE, + KEY `idx_parent_id` (`parent_id`) USING BTREE, + KEY `idx_show_order` (`show_order`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='部门管理表'; + +-- ---------------------------- +-- Records of zz_sys_dept +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_dept` VALUES (1808020008341606402, NULL, '公司总部', 1, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_dept_post +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_dept_post`; +CREATE TABLE `zz_sys_dept_post` ( + `dept_post_id` bigint NOT NULL COMMENT '主键Id', + `dept_id` bigint NOT NULL COMMENT '部门Id', + `post_id` bigint NOT NULL COMMENT '岗位Id', + `post_show_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门岗位显示名称', + PRIMARY KEY (`dept_post_id`) USING BTREE, + KEY `idx_post_id` (`post_id`) USING BTREE, + KEY `idx_dept_id` (`dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_sys_dept_post +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_dept_post` VALUES (1809038003536924672, 1808020008341606402, 1809037927934595072, '领导岗位'); +INSERT INTO `zz_sys_dept_post` VALUES (1809038003968937984, 1808020008341606402, 1809037967663042560, '普通员工'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_dept_relation +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_dept_relation`; +CREATE TABLE `zz_sys_dept_relation` ( + `parent_dept_id` bigint NOT NULL COMMENT '父部门Id', + `dept_id` bigint NOT NULL COMMENT '部门Id', + PRIMARY KEY (`parent_dept_id`,`dept_id`), + KEY `idx_dept_id` (`dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='部门关联关系表'; + +-- ---------------------------- +-- Records of zz_sys_dept_relation +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_dept_relation` VALUES (1808020008341606402, 1808020008341606402); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_menu`; +CREATE TABLE `zz_sys_menu` ( + `menu_id` bigint NOT NULL COMMENT '主键Id', + `parent_id` bigint DEFAULT NULL COMMENT '父菜单Id,目录菜单的父菜单为null', + `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '菜单显示名称', + `menu_type` int NOT NULL COMMENT '(0: 目录 1: 菜单 2: 按钮 3: UI片段)', + `form_router_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '前端表单路由名称,仅用于menu_type为1的菜单类型', + `online_form_id` bigint DEFAULT NULL COMMENT '在线表单主键Id', + `online_menu_perm_type` int DEFAULT NULL COMMENT '在线表单菜单的权限控制类型', + `report_page_id` bigint DEFAULT NULL COMMENT '统计页面主键Id', + `online_flow_entry_id` bigint DEFAULT NULL COMMENT '仅用于在线表单的流程Id', + `show_order` int NOT NULL COMMENT '菜单显示顺序 (值越小,排序越靠前)', + `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '菜单图标', + `extra_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '附加信息', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`menu_id`) USING BTREE, + KEY `idx_show_order` (`show_order`) USING BTREE, + KEY `idx_parent_id` (`parent_id`) USING BTREE, + KEY `idx_menu_type` (`menu_type`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='菜单和操作权限管理表'; + +-- ---------------------------- +-- Records of zz_sys_menu +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_menu` VALUES (1392786476428693504, NULL, '在线表单', 0, NULL, NULL, NULL, NULL, NULL, 2, 'el-icon-c-scale-to-original', NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1392786549942259712, 1392786476428693504, '字典管理', 1, 'formOnlineDict', NULL, NULL, NULL, NULL, 2, NULL, '{\"permCodeList\":[\"onlineDict.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1392786950682841088, 1392786476428693504, '表单管理', 1, 'formOnlinePage', NULL, NULL, NULL, NULL, 3, NULL, '{\"permCodeList\":[\"onlinePage.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418057714138877952, NULL, '流程管理', 0, NULL, NULL, NULL, NULL, NULL, 3, 'el-icon-s-operation', NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418057835631087616, 1418057714138877952, '流程分类', 1, 'formFlowCategory', NULL, NULL, NULL, NULL, 1, NULL, '{\"permCodeList\":[\"flowCategory.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418058289182150656, 1418057714138877952, '流程设计', 1, 'formFlowEntry', NULL, NULL, NULL, NULL, 2, NULL, '{\"permCodeList\":[\"flowEntry.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418058744037642240, 1418057714138877952, '流程实例', 1, 'formAllInstance', NULL, NULL, NULL, NULL, 3, NULL, '{\"permCodeList\":[\"flowOperation.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418059005175009280, NULL, '任务管理', 0, NULL, NULL, NULL, NULL, NULL, 4, 'el-icon-tickets', NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418059167532322816, 1418059005175009280, '待办任务', 1, 'formMyTask', NULL, NULL, NULL, NULL, 1, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418059283920064512, 1418059005175009280, '历史任务', 1, 'formMyHistoryTask', NULL, NULL, NULL, NULL, 3, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1423161217970606080, 1418059005175009280, '已办任务', 1, 'formMyApprovedTask', NULL, NULL, NULL, NULL, 2, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1634009076981567488, 1392786476428693504, '数据库链接', 1, 'formOnlineDblink', NULL, NULL, NULL, NULL, 1, NULL, '{\"permCodeList\":[\"onlineDblink.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020011080486913, NULL, '系统管理', 0, NULL, NULL, NULL, NULL, NULL, 1, 'el-icon-setting', '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317376, 1808020011080486913, '用户管理', 1, 'formSysUser', NULL, NULL, NULL, NULL, 100, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317377, 1808020011080486913, '部门管理', 1, 'formSysDept', NULL, NULL, NULL, NULL, 105, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317378, 1808020011080486913, '角色管理', 1, 'formSysRole', NULL, NULL, NULL, NULL, 110, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317379, 1808020011080486913, '数据权限管理', 1, 'formSysDataPerm', NULL, NULL, NULL, NULL, 115, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317380, 1808020011080486913, '岗位管理', 1, 'formSysPost', NULL, NULL, NULL, NULL, 106, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317381, 1808020011080486913, '菜单管理', 1, 'formSysMenu', NULL, NULL, NULL, NULL, 120, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317384, 1808020011080486913, '字典管理', 1, 'formSysDict', NULL, NULL, NULL, NULL, 135, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317385, 1808020011080486913, '操作日志', 1, 'formSysOperationLog', NULL, NULL, NULL, NULL, 140, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317386, 1808020011080486913, '在线用户', 1, 'formSysLoginUser', NULL, NULL, NULL, NULL, 145, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148866, 1808020012825317376, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser\",\"permCodeList\":[\"sysUser.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148867, 1808020012825317376, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:add\",\"permCodeList\":[\"sysUser.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148868, 1808020012825317376, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:update\",\"permCodeList\":[\"sysUser.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148869, 1808020012825317376, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:delete\",\"permCodeList\":[\"sysUser.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148870, 1808020012825317376, '重置密码', 3, NULL, NULL, NULL, NULL, NULL, 5, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:resetPassword\",\"permCodeList\":[\"sysUser.resetPassword\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148872, 1808020012825317377, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept\",\"permCodeList\":[\"sysDept.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148873, 1808020012825317377, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, '', '{\"bindType\":0,\"menuCode\":\"formSysDept:fragmentSysDept:add\",\"permCodeList\":[\"sysDept.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-05 09:51:07'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148874, 1808020012825317377, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:update\",\"permCodeList\":[\"sysDept.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148875, 1808020012825317377, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:delete\",\"permCodeList\":[\"sysDept.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148876, 1808020012825317377, '设置岗位', 3, NULL, NULL, NULL, NULL, NULL, 5, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:editPost\",\"permCodeList\":[\"sysDept.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148877, 1808020012825317377, '查看岗位', 3, NULL, NULL, NULL, NULL, NULL, 6, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:viewPost\",\"permCodeList\":[\"sysDept.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148879, 1808020012825317378, '角色管理', 2, NULL, NULL, NULL, NULL, NULL, 1, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148880, 1808020012825317378, '用户授权', 2, NULL, NULL, NULL, NULL, NULL, 2, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148881, 1808020075098148879, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole\",\"permCodeList\":[\"sysRole.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148882, 1808020075098148879, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole:add\",\"permCodeList\":[\"sysRole.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148883, 1808020075098148879, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole:update\",\"permCodeList\":[\"sysRole.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148884, 1808020075098148879, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole:delete\",\"permCodeList\":[\"sysRole.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148885, 1808020075098148880, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRoleUser\",\"permCodeList\":[\"sysRole.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148886, 1808020075098148880, '授权用户', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRoleUser:addUserRole\",\"permCodeList\":[\"sysRole.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148887, 1808020075098148880, '移除用户', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRoleUser:deleteUserRole\",\"permCodeList\":[\"sysRole.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148889, 1808020012825317379, '数据权限管理', 2, NULL, NULL, NULL, NULL, NULL, 1, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148890, 1808020012825317379, '用户授权', 2, NULL, NULL, NULL, NULL, NULL, 2, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148891, 1808020075098148889, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm\",\"permCodeList\":[\"sysDataPerm.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148892, 1808020075098148889, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm:add\",\"permCodeList\":[\"sysDataPerm.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148893, 1808020075098148889, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm:update\",\"permCodeList\":[\"sysDataPerm.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148894, 1808020075098148889, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm:delete\",\"permCodeList\":[\"sysDataPerm.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148895, 1808020075098148890, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPermUser\",\"permCodeList\":[\"sysDataPerm.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148896, 1808020075098148890, '授权用户', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPermUser:addDataPermUser\",\"permCodeList\":[\"sysDataPerm.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148897, 1808020075098148890, '移除用户', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPermUser:deleteDataPermUser\",\"permCodeList\":[\"sysDataPerm.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148899, 1808020012825317380, '岗位管理', 2, NULL, NULL, NULL, NULL, NULL, 1, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148900, 1808020075098148899, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost\",\"permCodeList\":[\"sysPost.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148901, 1808020075098148899, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost:add\",\"permCodeList\":[\"sysPost.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148902, 1808020075098148899, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost:update\",\"permCodeList\":[\"sysPost.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148903, 1808020075098148899, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost:delete\",\"permCodeList\":[\"sysPost.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148905, 1808020012825317381, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu\",\"permCodeList\":[\"sysMenu.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148906, 1808020012825317381, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu:add\",\"permCodeList\":[\"sysMenu.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148907, 1808020012825317381, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu:update\",\"permCodeList\":[\"sysMenu.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148908, 1808020012825317381, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu:delete\",\"permCodeList\":[\"sysMenu.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343171, 1808020012825317384, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict\",\"permCodeList\":[\"globalDict.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343172, 1808020012825317384, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:add\",\"permCodeList\":[\"globalDict.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343173, 1808020012825317384, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:update\",\"permCodeList\":[\"globalDict.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343174, 1808020012825317384, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:delete\",\"permCodeList\":[\"globalDict.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343175, 1808020012825317384, '同步缓存', 3, NULL, NULL, NULL, NULL, NULL, 5, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:reloadCache\",\"permCodeList\":[\"globalDict.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343177, 1808020012825317385, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysOperationLog:fragmentSysOperationLog\",\"permCodeList\":[\"sysOperationLog.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343179, 1808020012825317386, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysLoginUser:fragmentLoginUser\",\"permCodeList\":[\"loginUser.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343180, 1808020012825317386, '强制下线', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysLoginUser:fragmentLoginUser:delete\",\"permCodeList\":[\"loginUser.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_operation_log +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_operation_log`; +CREATE TABLE `zz_sys_operation_log` ( + `log_id` bigint NOT NULL COMMENT '主键Id', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '日志描述', + `operation_type` int DEFAULT NULL COMMENT '操作类型', + `service_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '接口所在服务名称', + `api_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '调用的controller全类名', + `api_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '调用的controller中的方法', + `session_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户会话sessionId', + `trace_id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '每次请求的Id', + `elapse` int DEFAULT NULL COMMENT '调用时长', + `request_method` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'HTTP 请求方法,如GET', + `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'HTTP 请求地址', + `request_arguments` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT 'controller接口参数', + `response_result` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'controller应答结果', + `request_ip` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求IP', + `success` bit(1) DEFAULT NULL COMMENT '应答状态', + `error_msg` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '错误信息', + `tenant_id` bigint DEFAULT NULL COMMENT '租户Id', + `operator_id` bigint DEFAULT NULL COMMENT '操作员Id', + `operator_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作员名称', + `operation_time` datetime DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`log_id`), + KEY `idx_trace_id_idx` (`trace_id`), + KEY `idx_operation_type_idx` (`operation_type`), + KEY `idx_operation_time_idx` (`operation_time`) USING BTREE, + KEY `idx_success` (`success`) USING BTREE, + KEY `idx_elapse` (`elapse`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='系统操作日志表'; + +-- ---------------------------- +-- Records of zz_sys_operation_log +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_operation_log` VALUES (1809037495178891264, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'c5eafaee0e294b3b8fe1ddc47a73aa6f', 526, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"U7kblCgd8NWaoUrEH%2B0j0ocRESztOUkH4L1eMANf40rAVWfgTmw8w1D2QeH2b99bxJQRCoELhiJDo3NbdN8sodZf%2BWa%2BRoH8URHmG1qziSMw4C%2Fc40gR1x4vclxMrq9jN1d3yP2gVljlaxVmMQcVsLqGsgcxfvyucwYzClifRUY%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 09:32:04'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037516607590400, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'f1104bc680014a999321a6ca3c240485', 136, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"MYjsPjZgslAadC1%2FhwPRNyG5yvtl%2BRVWJGOj0MfPNNJyTMMBgPrymEsoMsR%2FnSog7TdIborw%2BYgO9o31KFowqf3I3Gw6oI0qXkDbJKBqeDqkKKoOa95J9ITm7TKHKYcKu15xhmQvmU1OIMs59A2w39Cx1Z58I7gtbtHHL34iVJg%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 09:32:09'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037535469375488, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '58de29f3ec22457d8f4f980a350cf623', 579, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"i%2BcOZFUuWVmCCh%2B1ZhtpXTD8RNG1S4GMABC0dZssCPYckczkR%2FeRSuiYCMlDLaUa1oN%2BPeZRvj3zPKmDcuDyi0Jewxq7kTFyFAy%2Fbrep5MD3i2X%2BtV9B%2FT3CMMdbdOMa1OVP1AUO%2FBbmGdu0iK3UpvL608mJx1vqbpLRynYBazc%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:32:13'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037772132978688, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysRoleController', 'com.orangeforms.webadmin.upms.controller.SysRoleController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', 'cd5eb86b69094458881aa6dcb04aa766', 5496, 'POST', '/admin/upms/sysRole/add', '{\"menuIdListString\":\"1392786476428693504,1392786549942259712,1392786950682841088,1634009076981567488,1418057714138877952,1418057835631087616,1418058289182150656,1418058744037642240,1418059005175009280,1418059167532322816,1418059283920064512,1423161217970606080,1808020011080486913,1808020012825317376,1808020075098148866,1808020075098148867,1808020075098148868,1808020075098148869,1808020075098148870,1808020012825317377,1808020075098148872,1808020075098148873,1808020075098148874,1808020075098148875,1808020075098148876,1808020075098148877,1808020012825317378,1808020075098148879,1808020075098148881,1808020075098148882,1808020075098148883,1808020075098148884,1808020075098148880,1808020075098148885,1808020075098148886,1808020075098148887,1808020012825317379,1808020075098148889,1808020075098148891,1808020075098148892,1808020075098148893,1808020075098148894,1808020075098148890,1808020075098148895,1808020075098148896,1808020075098148897,1808020012825317380,1808020075098148899,1808020075098148900,1808020075098148901,1808020075098148902,1808020075098148903,1808020012825317381,1808020075098148905,1808020075098148906,1808020075098148907,1808020075098148908,1808020012825317384,1808020075102343171,1808020075102343172,1808020075102343173,1808020075102343174,1808020075102343175,1808020012825317385,1808020075102343177,1808020012825317386,1808020075102343179,1808020075102343180\",\"sysRoleDto\":{\"roleName\":\"查看全部\"}}', '{\"data\":1809037772728569856,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:10'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037881738530816, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysDataPermController', 'com.orangeforms.webadmin.upms.controller.SysDataPermController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '85746a39a3a34191884eb30453ecc237', 220, 'POST', '/admin/upms/sysDataPerm/add', '{\"sysDataPermDto\":{\"dataPermName\":\"查看全部\",\"ruleType\":0},\"menuIdListString\":\"\"}', '{\"data\":1809037881759502336,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:36'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037927917817856, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysPostController', 'com.orangeforms.webadmin.upms.controller.SysPostController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', 'fa4bf0f0b80748249bdfb4c78bb93b8d', 190, 'POST', '/admin/upms/sysPost/add', '{\"sysPostDto\":{\"leaderPost\":true,\"postLevel\":1,\"postName\":\"领导岗位\"}}', '{\"data\":1809037927934595072,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:47'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037967658848256, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysPostController', 'com.orangeforms.webadmin.upms.controller.SysPostController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '04da87ca21294e0296af2e162999e396', 228, 'POST', '/admin/upms/sysPost/add', '{\"sysPostDto\":{\"postLevel\":10,\"postName\":\"普通员工\"}}', '{\"data\":1809037967663042560,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:56'); +INSERT INTO `zz_sys_operation_log` VALUES (1809038123905060864, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysUserController', 'com.orangeforms.webadmin.upms.controller.SysUserController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '99bb05eadf944b54a194db3152773b13', 635, 'POST', '/admin/upms/sysUser/add', '{\"sysUserDto\":{\"deptId\":1808020008341606402,\"loginName\":\"userA\",\"password\":\"123456\",\"showName\":\"员工A\",\"userStatus\":0,\"userType\":2},\"dataPermIdListString\":\"1809037881759502336\",\"deptPostIdListString\":\"1809038003968937984\",\"roleIdListString\":\"1809037772728569856\"}', '{\"data\":1809038124504846336,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:34:34'); +INSERT INTO `zz_sys_operation_log` VALUES (1809042287854882816, '', 15, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysMenuController', 'com.orangeforms.webadmin.upms.controller.SysMenuController.update', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '68dd67ca9821460caee6986c5c3c3354', 420, 'POST', '/admin/upms/sysMenu/update', '{\"sysMenuDto\":{\"extraData\":\"{\\\"bindType\\\":0,\\\"menuCode\\\":\\\"formSysDept:fragmentSysDept:add\\\",\\\"permCodeList\\\":[\\\"sysDept.add\\\"]}\",\"icon\":\"\",\"menuId\":1808020075098148873,\"menuName\":\"新增\",\"menuType\":3,\"parentId\":1808020012825317377,\"showOrder\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:51:06'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050375580291072, '', 5, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogout', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '8611984bfad74113bcc5f5a2d30f0557', 36, 'POST', '/admin/upms/login/doLogout', '{}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 10:23:15'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050381297127424, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'bb940a20dbac4f11b7d448ebe11668c4', 466, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"jiRS2mxriWjx778WM%2FJql65bpRfu7BaqVkPrDySclvJ7%2B%2B0KSuAIZ557bEFocQnCWbfLJwRFokTUDastSpEeiFAsd1kwv6oZyQimj4KCyDtin6P6gPsn2GRQrFKACkOKBXY70FeGgQvaVwWBEGo6EzdfJw9adJOGf2WIigrIajk%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 10:23:16'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050432673157120, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'ffe77e34ec35454da6e71b0cef7f2ea8', 143, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"Kdkf8xz%2Fay2lKRidpUGWBJM7%2BlvxTVpjdSNLCuL1yx6LbVvTPo7PD5zFBLKMPWeSrtostyAFybz6lAAHpdCnQWjmbBbpMExTmY74O12EQySXOQBwrmH3yltq9MXJI5qRJ24imMxYyTvcX2yDMbEfDF3zcC404GvTgX0gexCmTjs%3D\",\"loginName\":\"userA\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 10:23:28'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050456257728512, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'b43789173f9244aa803866db7bafca73', 460, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"gYCq1nWZHSsvg35HCgnRzw23kN3PRTZJY%2Bt2bcZWliYf11o14OHEDhsH12nCC4LYn00UEDoYWbbMdiwNzQFmcgmbJq4%2Fu6uxURokHpI%2BEexZnL5IzWBb2P53hGBwUkOO36jRfbTm%2B0qRtIbpATs74jpc1L%2FFbT18%2Fj%2FN9C3bpq4%3D\",\"loginName\":\"userA\"}', NULL, '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:23:34'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050496074256384, '', 15, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysUserController', 'com.orangeforms.webadmin.upms.controller.SysUserController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '8926e20ea352417aab2234d2de2c1fea', 903, 'POST', '/admin/upms/sysUser/update', '{\"sysUserDto\":{\"deptId\":1808020008341606402,\"loginName\":\"userA\",\"showName\":\"员工A\",\"userId\":1809038124504846336,\"userStatus\":0,\"userType\":2},\"dataPermIdListString\":\"1809037881759502336\",\"deptPostIdListString\":\"1809038003968937984\",\"roleIdListString\":\"1809037772728569856\"}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:23:43'); +INSERT INTO `zz_sys_operation_log` VALUES (1809051198259466240, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowCategoryController', 'com.orangeforms.common.flow.controller.FlowCategoryController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'cd9d22e5fd194e7dac6c90531cab52dd', 249, 'POST', '/admin/flow/flowCategory/add', '{\"flowCategoryDto\":{\"code\":\"TEST\",\"name\":\"测试分类\",\"showOrder\":1}}', '{\"data\":1809051198460792832,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:26:31'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052045043306496, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '66225485743a44df81882b0b70885c69', 478, 'POST', '/admin/flow/flowEntry/add', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\"}}', '{\"data\":1809052045395628032,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:29:53'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052080904605696, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryVariableController', 'com.orangeforms.common.flow.controller.FlowEntryVariableController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'c7804ebbe24d492b82c23cb5f3b25879', 225, 'POST', '/admin/flow/flowEntryVariable/add', '{\"flowEntryVariableDto\":{\"builtin\":false,\"entryId\":1809052045395628032,\"showName\":\"AAA\",\"variableName\":\"aaa\",\"variableType\":1}}', '{\"data\":1809052080921382912,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:30:01'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052112206696448, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '9c28172a4ac74b448439334f0ea44d34', 306, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11}],\\\"notifyTypes\\\":[]}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:30:09'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052122159779840, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'c9af56a061bb4a87a964cb617f4f9c15', 201, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11,\\\"_X_ROW_KEY\\\":\\\"row_33\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:30:11'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052746851028992, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '6609a8762f6c49af9f570b746a30b8ac', 297, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11,\\\"_X_ROW_KEY\\\":\\\"row_33\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\",\"status\":0}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:32:40'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052753826156544, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'b1c15eb123b14ae68876d1e026b8f498', 267, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11,\\\"_X_ROW_KEY\\\":\\\"row_33\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\",\"status\":0}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:32:42'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055300347498496, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineDblinkController', 'com.orangeforms.common.online.controller.OnlineDblinkController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '4552709db33b4ac38520c97cbfbd84fb', 180, 'POST', '/admin/online/onlineDblink/add', '{\"onlineDblinkDto\":{\"configuration\":\"{\\\"sid\\\":true,\\\"initialPoolSize\\\":5,\\\"minPoolSize\\\":5,\\\"maxPoolSize\\\":50,\\\"host\\\":\\\"121.37.102.103\\\",\\\"port\\\":3306,\\\"database\\\":\\\"zzdemo-online-open\\\",\\\"username\\\":\\\"root\\\",\\\"password\\\":\\\"TianLiujielei231\\\"}\",\"dblinkName\":\"mysql-test\",\"dblinkType\":0}}', '{\"data\":1809055300360081408,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:42:49'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055451015286784, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'f6a28f34b7d94f0ca0ea0efdb23b59b6', 307, 'POST', '/admin/online/onlinePage/add', '{\"onlinePageDto\":{\"pageCode\":\"test\",\"pageName\":\"test\",\"pageType\":1,\"status\":1}}', '{\"data\":1809055451229196288,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:43:25'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055625460584448, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineDatasourceController', 'com.orangeforms.common.online.controller.OnlineDatasourceController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'cc233f444b9741e79e85902c6ced44c5', 2989, 'POST', '/admin/online/onlineDatasource/add', '{\"onlineDatasourceDto\":{\"datasourceName\":\"test\",\"dblinkId\":1809055300360081408,\"masterTableName\":\"zz_flow_entry\",\"variableName\":\"test\"},\"pageId\":1809055451229196288}', '{\"data\":1809055636340609024,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:44:06'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055701822083072, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'b9943033448544c5a5dcb93fe450bbeb', 245, 'POST', '/admin/online/onlinePage/update', '{\"onlinePageDto\":{\"pageCode\":\"test\",\"pageId\":1809055451229196288,\"pageName\":\"test\",\"pageType\":1,\"status\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:44:25'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055740065746944, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'abc44f1518ed471597d38fad6161e8ae', 589, 'POST', '/admin/online/onlineForm/add', '{\"onlineFormDto\":{\"datasourceIdList\":[1809055636340609024],\"formCode\":\"aaa\",\"formKind\":5,\"formName\":\"aaa\",\"formType\":1,\"masterTableId\":1809055626488188928,\"pageId\":1809055451229196288,\"paramsJson\":\"[]\",\"widgetJson\":\"{\\\"pc\\\":{\\\"filterItemWidth\\\":350,\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"tableWidget\\\":{\\\"widgetType\\\":100,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"operationList\\\":[{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"danger\\\",\\\"plain\\\":true,\\\"readOnly\\\":false,\\\"showOrder\\\":0,\\\"eventList\\\":[]},{\\\"id\\\":2,\\\"type\\\":0,\\\"name\\\":\\\"新建\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":false,\\\"readOnly\\\":false,\\\"showOrder\\\":1,\\\"eventList\\\":[]},{\\\"id\\\":3,\\\"type\\\":1,\\\"name\\\":\\\"编辑\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn success\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":10,\\\"eventList\\\":[]},{\\\"id\\\":4,\\\"type\\\":2,\\\"name\\\":\\\"删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn delete\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":15,\\\"eventList\\\":[]}],\\\"showName\\\":\\\"表格组件\\\",\\\"variableName\\\":\\\"table1720147467397\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"paddingBottom\\\":0,\\\"paged\\\":true,\\\"pageSize\\\":10,\\\"operationColumnWidth\\\":160,\\\"tableColumnList\\\":[]},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":true},\\\"leftWidget\\\":{\\\"widgetType\\\":13,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"showName\\\":\\\"树形选择组件\\\",\\\"variableName\\\":\\\"tree1720147467397\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"dictInfo\\\":{},\\\"required\\\":false,\\\"disabled\\\":false},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},\\\"operationList\\\":[{\\\"id\\\":0,\\\"type\\\":3,\\\"name\\\":\\\"导出\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":true,\\\"paramList\\\":[],\\\"eventList\\\":[],\\\"readOnly\\\":false,\\\"showOrder\\\":0},{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"en', '{\"data\":1809055741093351424,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:44:34'); +INSERT INTO `zz_sys_operation_log` VALUES (1809056459653124096, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.delete', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '406cd42f8c3a408fa8928e94a8ebdcc6', 329, 'POST', '/admin/online/onlineForm/delete', '{\"formId\":1809055741093351424}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:47:25'); +INSERT INTO `zz_sys_operation_log` VALUES (1809056484886056960, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.delete', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'ac3418b76d474c99862e49acab906011', 76, 'POST', '/admin/online/onlineForm/delete', '{\"formId\":1809055741093351424}', '{\"errorCode\":\"DATA_NOT_EXIST\",\"errorMessage\":\"数据不存在,请刷新后重试!\",\"success\":false}', '192.168.43.167', b'0', '数据不存在,请刷新后重试!', NULL, 1809038124504846336, 'userA', '2024-07-05 10:47:31'); +INSERT INTO `zz_sys_operation_log` VALUES (1809056769645744128, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '5ac915cc240f4463817134fbcba16d94', 508, 'POST', '/admin/online/onlineForm/add', '{\"onlineFormDto\":{\"datasourceIdList\":[1809055636340609024],\"formCode\":\"aaa\",\"formKind\":5,\"formName\":\"aaa\",\"formType\":1,\"masterTableId\":1809055626488188928,\"pageId\":1809055451229196288,\"paramsJson\":\"[]\",\"widgetJson\":\"{\\\"pc\\\":{\\\"filterItemWidth\\\":350,\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"tableWidget\\\":{\\\"widgetType\\\":100,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"operationList\\\":[{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"danger\\\",\\\"plain\\\":true,\\\"readOnly\\\":false,\\\"showOrder\\\":0,\\\"eventList\\\":[]},{\\\"id\\\":2,\\\"type\\\":0,\\\"name\\\":\\\"新建\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":false,\\\"readOnly\\\":false,\\\"showOrder\\\":1,\\\"eventList\\\":[]},{\\\"id\\\":3,\\\"type\\\":1,\\\"name\\\":\\\"编辑\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn success\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":10,\\\"eventList\\\":[]},{\\\"id\\\":4,\\\"type\\\":2,\\\"name\\\":\\\"删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn delete\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":15,\\\"eventList\\\":[]}],\\\"showName\\\":\\\"表格组件\\\",\\\"variableName\\\":\\\"table1720147715974\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"paddingBottom\\\":0,\\\"paged\\\":true,\\\"pageSize\\\":10,\\\"operationColumnWidth\\\":160,\\\"tableColumnList\\\":[]},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":true},\\\"leftWidget\\\":{\\\"widgetType\\\":13,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"showName\\\":\\\"树形选择组件\\\",\\\"variableName\\\":\\\"tree1720147715974\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"dictInfo\\\":{},\\\"required\\\":false,\\\"disabled\\\":false},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},\\\"operationList\\\":[{\\\"id\\\":0,\\\"type\\\":3,\\\"name\\\":\\\"导出\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":true,\\\"paramList\\\":[],\\\"eventList\\\":[],\\\"readOnly\\\":false,\\\"showOrder\\\":0},{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"en', '{\"data\":1809056770480410624,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:48:39'); +INSERT INTO `zz_sys_operation_log` VALUES (1809057010251993088, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.clone', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '32321e03b4ac418ca5d6fea8ac744a09', 453, 'POST', '/admin/online/onlineForm/clone', '{\"formId\":1809056770480410624}', '{\"data\":1809057010814029824,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:49:37'); +INSERT INTO `zz_sys_operation_log` VALUES (1809057028065202176, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.delete', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '45d08665cef04be4b9a20cc8b9e3505d', 302, 'POST', '/admin/online/onlineForm/delete', '{\"formId\":1809057010814029824}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:49:41'); +INSERT INTO `zz_sys_operation_log` VALUES (1809131899889651712, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, '17cfa5afe5374abda116be3f69094fcf', 497, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"ekih%2BFzFR03abVnW2zJLYZJ%2FEHw2EpMZKuW9698GRI6zsXrhLXX1UjKEN11L31%2BrePfnFLvp%2Bk408bZ6CLtfjhTjRR9wbOzPocmtbK063VM%2F7Crw9nAlaSEobYPwWlHuiugw8CcVPPWAAfiSz2yoedg5%2BBbBDx4SnWKKPz7K59Y%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 15:47:12'); +INSERT INTO `zz_sys_operation_log` VALUES (1809131924610879488, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'd2af8f5c698e4e6b8b610163e5908217', 547, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"n1NyIK3vu4fhzT5kFQRSYQvehBUqZ2RK6VOeDT7NKd7Tj7Z78CV6Yg73TdJSKLH7PtQ1yrzCPE7QijTH3CCPqg6x%2FDE0ndlm0GPAmdcG8c1LKu4RrV%2BM37grdKeOtbCbohG4uishREJ9jovLiZI8twfRGCnzqEs3bKBjPybBdDw%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:47:18'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132075907813376, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.delete', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f9258eaefc164f9db3a6792c03dbaa82', 1429, 'POST', '/admin/online/onlinePage/delete', '{\"pageId\":1809055451229196288}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:47:54'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132176877293568, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f41bdd39fc984cf38b2cca8ccbefaaa1', 343, 'POST', '/admin/online/onlinePage/add', '{\"onlinePageDto\":{\"pageCode\":\"flowLeave\",\"pageName\":\"请假申请\",\"pageType\":10,\"status\":1}}', '{\"data\":1809132177523216384,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:18'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132250441191424, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineDatasourceController', 'com.orangeforms.common.online.controller.OnlineDatasourceController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '0db3da9d6b6048bab73a3fe445297ae1', 1653, 'POST', '/admin/online/onlineDatasource/add', '{\"onlineDatasourceDto\":{\"datasourceName\":\"请假申请\",\"dblinkId\":1809055300360081408,\"masterTableName\":\"zz_test_flow_leave\",\"variableName\":\"dsLeave\"},\"pageId\":1809132177523216384}', '{\"data\":1809132255981867008,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:35'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132300521181184, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '58819dae9e9f442fb21f5cadb3c5d326', 270, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"请假用户\",\"columnId\":1809132252425097216,\"columnName\":\"user_id\",\"columnShowOrder\":2,\"columnType\":\"bigint\",\"deptFilter\":false,\"fieldKind\":21,\"filterType\":0,\"fullColumnType\":\"bigint\",\"nullable\":false,\"numericPrecision\":19,\"objectFieldName\":\"userId\",\"objectFieldType\":\"Long\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:47'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132348223000576, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '4e7166af1f55482ea6189787d6a661fe', 267, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"开始时间\",\"columnId\":1809132253733720064,\"columnName\":\"leave_begin_time\",\"columnShowOrder\":5,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveBeginTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:59'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132364907941888, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '5ea928dfa368402b9bcd8a8259c93097', 256, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"结束时间\",\"columnId\":1809132254102818816,\"columnName\":\"leave_end_time\",\"columnShowOrder\":6,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveEndTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:03'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132399720665088, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'a0c6b421cd5f4f0aa6810db4abe0d301', 683, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"申请时间\",\"columnId\":1809132254388031488,\"columnName\":\"apply_time\",\"columnShowOrder\":7,\"columnType\":\"datetime\",\"deptFilter\":false,\"fieldKind\":20,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"applyTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:11'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132453835575296, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '4a0b9a2c8a614820941b95772b82e6ba', 366, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"最后审批状态\",\"columnId\":1809132254782296064,\"columnName\":\"approval_status\",\"columnShowOrder\":8,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":26,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"approvalStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:24'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132505723310080, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'e54070a44ae2487bb4810a3d920d7988', 1179, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"流程状态\",\"columnId\":1809132255327555584,\"columnName\":\"flow_status\",\"columnShowOrder\":9,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":26,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"flowStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:36'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132536761159680, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '5f0e57d9c5df44a3907bd4878183c68a', 271, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"流程状态\",\"columnId\":1809132255327555584,\"columnName\":\"flow_status\",\"columnShowOrder\":9,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":25,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"flowStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:43'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132559511064576, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '28063f6a43804db882504597d2d77353', 306, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"用户名\",\"columnId\":1809132255679877120,\"columnName\":\"username\",\"columnShowOrder\":10,\"columnType\":\"varchar\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"varchar(255)\",\"nullable\":true,\"objectFieldName\":\"username\",\"objectFieldType\":\"String\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:49'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132570206539776, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '48ce554a01704633a62a3aabbc394b2f', 210, 'POST', '/admin/online/onlinePage/update', '{\"onlinePageDto\":{\"pageCode\":\"flowLeave\",\"pageId\":1809132177523216384,\"pageName\":\"请假申请\",\"pageType\":10,\"status\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:51'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132634748489728, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '546500c7cc10417d91f89582652f820b', 506, 'POST', '/admin/online/onlineForm/add', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"paramsJson\":\"[]\",\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"customFieldList\\\":[],\\\"widgetList\\\":[],\\\"formEventList\\\":[],\\\"maskFieldList\\\":[],\\\"allowEventList\\\":[\\\"formCreated\\\",\\\"afterLoadFormData\\\",\\\"beforeCommitFormData\\\"],\\\"fullscreen\\\":true,\\\"supportOperation\\\":false,\\\"width\\\":800}}\"}}', '{\"data\":1809132635633487872,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:50:07'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133545088618496, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '413482d6eeac4cdcb05b7423026c3f8d', 306, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"请假类型\",\"columnId\":1809132253377204224,\"columnName\":\"leave_type\",\"columnShowOrder\":4,\"columnType\":\"int\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":false,\"numericPrecision\":10,\"objectFieldName\":\"leaveType\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:44'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133558430699520, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '9dad13d8ce3d4e94b0bf01b544f0c04b', 329, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"请假原因\",\"columnId\":1809132252852916224,\"columnName\":\"leave_reason\",\"columnShowOrder\":3,\"columnType\":\"varchar\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"varchar(512)\",\"nullable\":false,\"objectFieldName\":\"leaveReason\",\"objectFieldType\":\"String\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:47'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133570850033664, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '6573018ce2c547b8ab633c45affb8094', 294, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"开始时间\",\"columnId\":1809132253733720064,\"columnName\":\"leave_begin_time\",\"columnShowOrder\":5,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveBeginTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:50'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133584934506496, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '3c4aad5641704fd991b3b9717d60f039', 386, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"结束时间\",\"columnId\":1809132254102818816,\"columnName\":\"leave_end_time\",\"columnShowOrder\":6,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveEndTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:53'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133598788292608, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '1fd06e9d5bec413a8e1437968ee99920', 327, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"申请时间\",\"columnId\":1809132254388031488,\"columnName\":\"apply_time\",\"columnShowOrder\":7,\"columnType\":\"datetime\",\"deptFilter\":false,\"fieldKind\":20,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"applyTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:57'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133609777369088, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '67104da4ba934ce9904d3c5c646b4836', 281, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"最后审批状态\",\"columnId\":1809132254782296064,\"columnName\":\"approval_status\",\"columnShowOrder\":8,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":26,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"approvalStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:59'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133618182754304, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f3aea6a5c397400eb272d364f6b1870e', 218, 'POST', '/admin/online/onlinePage/update', '{\"onlinePageDto\":{\"pageCode\":\"flowLeave\",\"pageId\":1809132177523216384,\"pageName\":\"请假申请\",\"pageType\":10,\"status\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:54:01'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143621992058880, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'ec8cccdd9b5c42e69b2f100bc1307a59', 636, 'POST', '/admin/online/onlineForm/update', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formId\":1809132635633487872,\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"operationList\\\":[],\\\"customFieldList\\\":[],\\\"widgetList\\\":[{\\\"widgetType\\\":1,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132252852916224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假原因\\\",\\\"variableName\\\":\\\"leaveReason\\\",\\\"props\\\":{\\\"span\\\":24,\\\"type\\\":\\\"text\\\",\\\"placeholder\\\":\\\"\\\",\\\"show-password\\\":false,\\\"show-word-limit\\\":false,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false}],\\\"formEventList\\\":[],\\\"maskFieldList\\\":[],\\\"width\\\":800,\\\"fullscreen\\\":true}}\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:33:46'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143691172909056, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'dc39d01f441c439fa6f3e36eaf50529b', 405, 'POST', '/admin/online/onlineForm/update', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formId\":1809132635633487872,\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"operationList\\\":[],\\\"customFieldList\\\":[],\\\"widgetList\\\":[{\\\"widgetType\\\":3,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132253377204224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假类型\\\",\\\"variableName\\\":\\\"leaveType\\\",\\\"props\\\":{\\\"span\\\":24,\\\"placeholder\\\":\\\"\\\",\\\"step\\\":1,\\\"controls\\\":true,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},{\\\"widgetType\\\":1,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132252852916224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假原因\\\",\\\"variableName\\\":\\\"leaveReason\\\",\\\"props\\\":{\\\"span\\\":24,\\\"type\\\":\\\"text\\\",\\\"placeholder\\\":\\\"\\\",\\\"show-password\\\":false,\\\"show-word-limit\\\":false,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{}}],\\\"formEventList\\\":[],\\\"maskFieldList\\\":[],\\\"width\\\":800,\\\"fullscreen\\\":true}}\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:03'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143765026213888, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '38feac5b0a75448d8557d336ddbbff53', 590, 'POST', '/admin/online/onlineForm/update', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formId\":1809132635633487872,\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"operationList\\\":[],\\\"customFieldList\\\":[],\\\"widgetList\\\":[{\\\"widgetType\\\":3,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132253377204224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假类型\\\",\\\"variableName\\\":\\\"leaveType\\\",\\\"props\\\":{\\\"span\\\":24,\\\"placeholder\\\":\\\"\\\",\\\"step\\\":1,\\\"controls\\\":true,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{}},{\\\"widgetType\\\":1,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132252852916224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假原因\\\",\\\"variableName\\\":\\\"leaveReason\\\",\\\"props\\\":{\\\"span\\\":24,\\\"type\\\":\\\"text\\\",\\\"placeholder\\\":\\\"\\\",\\\"show-password\\\":false,\\\"show-word-limit\\\":false,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{}},{\\\"widgetType\\\":20,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132253733720064\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"开始时间\\\",\\\"variableName\\\":\\\"leaveBeginTime\\\",\\\"props\\\":{\\\"span\\\":12,\\\"placeholder\\\":\\\"\\\",\\\"type\\\":\\\"date\\\",\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},{\\\"widgetType\\\":20,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132254102818816\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"结束时间\\\",\\\"variableName\\\":\\\"leaveEndTime\\\",\\\"props\\\":{\\\"span\\\":12,\\\"placeholder\\\":\\\"\\\",\\\"type\\\":\\\"date\\\",\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eve', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:21'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143792943501312, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.updateStatus', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f373e5187505453b8a73de82f3487b77', 307, 'POST', '/admin/online/onlinePage/updatePublished', '{\"published\":true,\"pageId\":1809132177523216384}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:27'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143848773881856, '', 20, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.delete', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '9a4914d35bed44ad9363c8d9152f09ba', 374, 'POST', '/admin/flow/flowEntry/delete', '{\"entryId\":1809052045395628032}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:40'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143990952398848, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '280442aa890542d8b436c40e8b65c3c8', 564, 'POST', '/admin/flow/flowEntry/add', '{\"flowEntryDto\":{\"bindFormType\":0,\"categoryId\":1809051198460792832,\"defaultFormId\":1809132635633487872,\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"LL\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\"}],\\\"notifyTypes\\\":[\\\"email\\\"],\\\"cascadeDeleteBusinessData\\\":true,\\\"supportRevive\\\":false}\",\"pageId\":1809132177523216384,\"processDefinitionKey\":\"flowLeave\",\"processDefinitionName\":\"请假申请\"}}', '{\"data\":1809143991627681792,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:35:14'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144002897776640, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '059610f822e649e894e551173c416ac0', 249, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":0,\"categoryId\":1809051198460792832,\"defaultFormId\":1809132635633487872,\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"LL\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809143991627681792,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_57\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_58\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_59\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_60\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_61\\\"}],\\\"notifyTypes\\\":[\\\"email\\\"],\\\"cascadeDeleteBusinessData\\\":true,\\\"supportRevive\\\":false}\",\"pageId\":1809132177523216384,\"processDefinitionKey\":\"flowLeave\",\"processDefinitionName\":\"请假申请\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:35:17'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144278463549440, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'c0aff90acd7542fc905229d237403dcc', 304, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":0,\"bpmnXml\":\"\\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n ', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:36:35'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144345769545728, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'e16fbd18283347a6bb899a40fac80441', 238, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":0,\"bpmnXml\":\"\\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n ', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:36:39'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144417529892864, '', 65, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.publish', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'b71e69dbde6b4f868601d8c9a42f0e03', 3149, 'POST', '/admin/flow/flowEntry/publish', '{\"entryId\":1809143991627681792}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:36:56'); +INSERT INTO `zz_sys_operation_log` VALUES (1809146479772700672, '', 100, 'application-webadmin', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController.startPreview', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '8cd4a46b301a48de878e676ce5aadd47', 3835, 'POST', '/admin/flow/flowOnlineOperation/startPreview', '{\"masterData\":{\"leave_begin_time\":\"2024-07-05 00:00:00\",\"leave_type\":1,\"leave_reason\":\"111\",\"leave_end_time\":\"2024-07-08 00:00:00\"},\"flowTaskCommentDto\":{\"approvalType\":\"agree\"},\"taskVariableData\":{},\"processDefinitionKey\":\"flowLeave\",\"copyData\":{}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:45:08'); +INSERT INTO `zz_sys_operation_log` VALUES (1809146595745206272, '', 120, 'application-webadmin', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController.submitUserTask', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'eadd3fe01b3244a4826a245ae15c328f', 2462, 'POST', '/admin/flow/flowOnlineOperation/submitUserTask', '{\"processInstanceId\":\"e1fb2ada-3aaa-11ef-86ec-acde48001122\",\"masterData\":{\"leave_begin_time\":\"2024-07-05 00:00:00\",\"leave_type\":1,\"user_id\":\"1808020007993479168\",\"apply_time\":\"2024-07-05 16:45:08\",\"id\":\"1809146480452177920\",\"leave_reason\":\"111\",\"leave_end_time\":\"2024-07-08 00:00:00\"},\"flowTaskCommentDto\":{\"approvalType\":\"agree\",\"taskComment\":\"11\"},\"taskVariableData\":{},\"taskId\":\"e322e20d-3aaa-11ef-86ec-acde48001122\",\"copyData\":{}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:45:35'); +INSERT INTO `zz_sys_operation_log` VALUES (1809146772417679360, '', 120, 'application-webadmin', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController.submitUserTask', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'bb806b13041349cc8a12fa2d5de5a028', 2310, 'POST', '/admin/flow/flowOnlineOperation/submitUserTask', '{\"processInstanceId\":\"e1fb2ada-3aaa-11ef-86ec-acde48001122\",\"masterData\":{\"leave_begin_time\":\"2024-07-05 00:00:00\",\"leave_type\":1,\"user_id\":\"1808020007993479168\",\"apply_time\":\"2024-07-05 16:45:08\",\"id\":\"1809146480452177920\",\"leave_reason\":\"111\",\"leave_end_time\":\"2024-07-08 00:00:00\"},\"flowTaskCommentDto\":{\"approvalType\":\"agree\",\"taskComment\":\"44\"},\"taskVariableData\":{},\"taskId\":\"0669cc2a-3aab-11ef-86ec-acde48001122\",\"copyData\":{}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:46:18'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_perm_whitelist +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_perm_whitelist`; +CREATE TABLE `zz_sys_perm_whitelist` ( + `perm_url` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '权限资源的url', + `module_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限资源所属模块名字(通常是Controller的名字)', + `perm_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限的名称', + PRIMARY KEY (`perm_url`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='权限资源白名单表(认证用户均可访问的url资源)'; + +-- ---------------------------- +-- Table structure for zz_sys_post +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_post`; +CREATE TABLE `zz_sys_post` ( + `post_id` bigint NOT NULL COMMENT '岗位Id', + `post_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '岗位名称', + `post_level` int NOT NULL COMMENT '岗位层级,数值越小级别越高', + `leader_post` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否领导岗位', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`post_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_sys_post +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_post` VALUES (1809037927934595072, '领导岗位', 1, b'1', 1808020007993479168, '2024-07-05 09:33:47', 1808020007993479168, '2024-07-05 09:33:47'); +INSERT INTO `zz_sys_post` VALUES (1809037967663042560, '普通员工', 10, b'0', 1808020007993479168, '2024-07-05 09:33:56', 1808020007993479168, '2024-07-05 09:33:56'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_role`; +CREATE TABLE `zz_sys_role` ( + `role_id` bigint NOT NULL COMMENT '主键Id', + `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '角色名称', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='系统角色表'; + +-- ---------------------------- +-- Records of zz_sys_role +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_role` VALUES (1809037772728569856, '查看全部', 1808020007993479168, '2024-07-05 09:33:10', 1808020007993479168, '2024-07-05 09:33:10'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_role_menu`; +CREATE TABLE `zz_sys_role_menu` ( + `role_id` bigint NOT NULL COMMENT '角色Id', + `menu_id` bigint NOT NULL COMMENT '菜单Id', + PRIMARY KEY (`role_id`,`menu_id`) USING BTREE, + KEY `idx_menu_id` (`menu_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='角色与菜单对应关系表'; + +-- ---------------------------- +-- Records of zz_sys_role_menu +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1392786476428693504); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1392786549942259712); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1392786950682841088); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418057714138877952); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418057835631087616); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418058289182150656); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418058744037642240); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418059005175009280); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418059167532322816); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418059283920064512); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1423161217970606080); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1634009076981567488); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020011080486913); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317376); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317377); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317378); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317379); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317380); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317381); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317384); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317385); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317386); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148866); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148867); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148868); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148869); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148870); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148872); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148873); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148874); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148875); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148876); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148877); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148879); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148880); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148881); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148882); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148883); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148884); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148885); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148886); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148887); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148889); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148890); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148891); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148892); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148893); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148894); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148895); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148896); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148897); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148899); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148900); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148901); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148902); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148903); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148905); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148906); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148907); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148908); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343171); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343172); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343173); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343174); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343175); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343177); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343179); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343180); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_user +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_user`; +CREATE TABLE `zz_sys_user` ( + `user_id` bigint NOT NULL COMMENT '主键Id', + `login_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户登录名称', + `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '密码', + `show_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户显示名称', + `dept_id` bigint NOT NULL COMMENT '用户所在部门Id', + `head_image_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户头像的Url', + `user_type` int NOT NULL COMMENT '用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)', + `user_status` int NOT NULL COMMENT '状态(0: 正常 1: 锁定)', + `email` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户邮箱', + `mobile` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户手机', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`user_id`) USING BTREE, + UNIQUE KEY `uk_login_name` (`login_name`) USING BTREE, + KEY `idx_dept_id` (`dept_id`) USING BTREE, + KEY `idx_status` (`user_status`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='系统用户表'; + +-- ---------------------------- +-- Records of zz_sys_user +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_user` VALUES (1808020007993479168, 'admin', '$2a$10$C1/DwnlXP3s.HOFsmL60Resq0juaRt6/WK8JCzcNbgbpueUMs71Um', '管理员', 1808020008341606402, NULL, 0, 0, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1); +INSERT INTO `zz_sys_user` VALUES (1809038124504846336, 'userA', '$2a$10$perpVEYWNTE0.oP0C7L5beiv1EYs3XEn0qkgOKwB8Rm7p/BDGYLEa', '员工A', 1808020008341606402, NULL, 2, 0, NULL, NULL, 1808020007993479168, '2024-07-05 09:34:34', 1809038124504846336, '2024-07-05 10:23:44', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_user_post +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_user_post`; +CREATE TABLE `zz_sys_user_post` ( + `user_id` bigint NOT NULL COMMENT '用户Id', + `dept_post_id` bigint NOT NULL COMMENT '部门岗位Id', + `post_id` bigint NOT NULL COMMENT '岗位Id', + PRIMARY KEY (`user_id`,`dept_post_id`) USING BTREE, + KEY `idx_post_id` (`post_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_sys_user_post +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_user_post` VALUES (1809038124504846336, 1809038003968937984, 1809037967663042560); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_user_role`; +CREATE TABLE `zz_sys_user_role` ( + `user_id` bigint NOT NULL COMMENT '用户Id', + `role_id` bigint NOT NULL COMMENT '角色Id', + PRIMARY KEY (`user_id`,`role_id`) USING BTREE, + KEY `idx_role_id` (`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='用户与角色对应关系表'; + +-- ---------------------------- +-- Records of zz_sys_user_role +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_user_role` VALUES (1809038124504846336, 1809037772728569856); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_test_flow_leave +-- ---------------------------- +DROP TABLE IF EXISTS `zz_test_flow_leave`; +CREATE TABLE `zz_test_flow_leave` ( + `id` bigint NOT NULL COMMENT '主键Id', + `user_id` bigint NOT NULL COMMENT '请假用户Id', + `leave_reason` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '请假原因', + `leave_type` int NOT NULL COMMENT '请假类型', + `leave_begin_time` datetime NOT NULL COMMENT '请假开始时间', + `leave_end_time` datetime NOT NULL COMMENT '请假结束时间', + `apply_time` datetime NOT NULL COMMENT '申请时间', + `approval_status` int DEFAULT NULL COMMENT '最后审批状态', + `flow_status` int DEFAULT NULL COMMENT '流程状态', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_test_flow_leave +-- ---------------------------- +BEGIN; +INSERT INTO `zz_test_flow_leave` VALUES (1734132261424467969, 1440911410581213417, '测试', 1, '2023-12-11 00:00:00', '2024-01-02 00:00:00', '2023-12-11 16:45:24', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1734132937084899329, 1440911410581213417, '测试', 1, '2023-12-11 00:00:00', '2024-01-10 00:00:00', '2023-12-11 16:48:05', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1734760286021226497, 1440911410581213417, '22', 2, '2023-12-12 00:00:00', '2023-12-14 00:00:00', '2023-12-13 10:20:57', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1735571074717847553, 1440911410581213417, '123', 1, '2023-12-07 00:00:00', '2023-12-08 00:00:00', '2023-12-15 16:02:44', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1735644235845079041, 1440911410581213417, '111', 1, '2023-12-14 00:00:00', '2023-12-16 00:00:00', '2023-12-15 20:53:27', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1735959007710941185, 1440911410581213417, '123123', 2, '2023-12-16 00:00:00', '2023-12-22 00:00:00', '2023-12-16 17:44:15', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736002626216005633, 1440911410581213417, '213213', 1, '2023-12-15 00:00:00', '2024-01-18 00:00:00', '2023-12-16 20:37:34', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736249711238582272, 1440911410581213417, 'qqq', 2, '2023-12-15 00:00:00', '2024-01-17 00:00:00', '2023-12-17 12:59:24', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736653319645958144, 1440911410581213417, '呃呃呃', 1, '2023-12-18 00:00:00', '2023-12-20 00:00:00', '2023-12-18 15:43:12', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736916738529824769, 1440911410581213417, '请假', 2, '2023-12-21 00:00:00', '2023-12-23 00:00:00', '2023-12-19 09:09:55', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737101008917499905, 1440911410581213417, 'fff', 3, '2023-12-19 00:00:00', '2023-12-20 00:00:00', '2023-12-19 21:22:09', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737314824108380161, 1440911410581213417, '有事', 1, '2023-12-01 00:00:00', '2023-12-09 00:00:00', '2023-12-20 11:31:46', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737358381695373313, 1440911410581213417, '123', 2, '2023-12-13 00:00:00', '2024-01-19 00:00:00', '2023-12-20 14:24:51', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737615175483133953, 1440911410581213417, '尴尬', 1, '2023-12-21 00:00:00', '2023-12-22 00:00:00', '2023-12-21 07:25:16', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737641283461058561, 1440911410581213417, '测试', 1, '2023-12-21 00:00:00', '2023-12-28 00:00:00', '2023-12-21 09:09:00', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737646632062685184, 1440911410581213417, '风复古', 1, '2023-12-22 00:00:00', '2023-12-22 00:00:00', '2023-12-21 09:30:16', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737661659834486784, 1440911410581213417, '想咋就咋', 3, '2023-12-22 00:00:00', '2023-12-22 00:00:00', '2023-12-21 10:29:59', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737662716845232128, 1440911410581213417, '黑胡椒', 1, '2023-12-18 00:00:00', '2023-12-22 00:00:00', '2023-12-21 10:34:11', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666820992667648, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:29', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666823148539905, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:30', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666824016760833, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:30', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666824809484289, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:30', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737747164756447233, 1440911410581213417, 'c', 1, '2023-12-23 00:00:00', '2024-01-13 00:00:00', '2023-12-21 16:09:45', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738557159815254017, 1440911410581213417, '测试新增', 2, '2023-12-22 00:00:00', '2024-01-12 00:00:00', '2023-12-23 21:48:22', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738586314833399809, 1440911410581213417, '轻机枪', 1, '2023-12-22 00:00:00', '2023-12-29 00:00:00', '2023-12-23 23:44:13', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738590302731505665, 1440911410581213417, '测试', 2, '2023-12-23 00:00:00', '2024-01-04 00:00:00', '2023-12-24 00:00:04', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738593079201370113, 1440911410581213417, '测试', 1, '2024-01-04 00:00:00', '2024-01-11 00:00:00', '2023-12-24 00:11:06', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738597715752783872, 1440911410581213417, '消息 抄送发', 1, '2023-12-13 00:00:00', '2024-01-25 00:00:00', '2023-12-24 00:29:32', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738598397780168705, 1440911410581213417, ' 额', 1, '2023-12-13 00:00:00', '2023-12-13 00:00:00', '2023-12-24 00:32:14', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738614127170949120, 1440911410581213417, '超市那个', 1, '2023-12-13 00:00:00', '2023-12-24 00:00:00', '2023-12-24 01:34:44', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1739529776575549440, 1440911410581213417, '33232', 1, '2023-12-07 00:00:00', '2024-01-16 00:00:00', '2023-12-26 14:13:12', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1739534951415549952, 1440911410581213417, '111', 1, '2024-01-25 00:00:00', '2024-01-27 00:00:00', '2023-12-26 14:33:46', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1739860694376910849, 1440911410581213417, '111', 1, '2023-12-27 00:00:00', '2023-12-28 00:00:00', '2023-12-27 12:08:09', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1740031035300646913, 1440911410581213417, '测试抄送', 1, '2024-01-03 00:00:00', '2024-01-11 00:00:00', '2023-12-27 23:25:02', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741067028283789313, 1440911410581213417, '测试抄送', 1, '2023-12-29 00:00:00', '2024-02-08 00:00:00', '2023-12-30 20:01:42', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741068565080969217, 1440911410581213417, '亲近抄送', 1, '2024-02-08 00:00:00', '2024-01-19 00:00:00', '2023-12-30 20:07:48', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741075078512119809, 1440911410581213417, '测试抄送', 1, '2023-12-30 00:00:00', '2024-01-26 00:00:00', '2023-12-30 20:33:41', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741077243179831297, 1440911410581213417, '测试抄送', 1, '2023-12-30 00:00:00', '2024-01-12 00:00:00', '2023-12-30 20:42:17', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741082898645127169, 1440911410581213417, '11111', 1, '2023-12-13 00:00:00', '2023-12-29 00:00:00', '2023-12-30 21:04:45', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1742075427653947392, 1440911410581213417, '6666', 1, '2024-01-02 00:00:00', '2024-01-27 00:00:00', '2024-01-02 14:48:43', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743138899498110977, 1440911410581213417, '2222', 1, '2024-01-10 00:00:00', '2024-01-10 00:00:00', '2024-01-05 13:14:34', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236528957558784, 1440911410581213417, 'dsfsadffsdf', 1, '2024-01-09 00:00:00', '2024-01-31 00:00:00', '2024-01-05 19:42:31', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236847603027968, 1440911410581213417, 'sdfaff', 1, '2024-01-11 00:00:00', '2024-02-06 00:00:00', '2024-01-05 19:43:47', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236894344351745, 1440911410581213417, 'dsfsdfasdf', 1, '2024-01-11 00:00:00', '2024-02-14 00:00:00', '2024-01-05 19:43:58', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236965743988737, 1440911410581213417, 'zxczxc', 1, '2024-01-20 00:00:00', '2024-02-12 00:00:00', '2024-01-05 19:44:15', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743529562567872512, 1440911410581213417, '休息', 1, '2024-01-12 00:00:00', '2024-01-13 00:00:00', '2024-01-06 15:06:56', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743570048200478721, 1440911410581213417, '是一款..是,', 1, '2024-01-07 00:00:00', '2024-01-31 00:00:00', '2024-01-06 17:47:48', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743847321545740288, 1440911410581213417, '测试请假', 3, '2024-01-08 00:00:00', '2024-01-24 00:00:00', '2024-01-07 12:09:35', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743848671104995328, 1440911410581213417, '请假新增测试', 1, '2024-01-15 00:00:00', '2024-01-16 00:00:00', '2024-01-07 12:14:57', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743894439526404097, 1440911410581213417, '测试', 2, '2024-01-07 00:00:00', '2024-01-24 00:00:00', '2024-01-07 15:16:49', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745342183466078208, 1440911410581213417, 'asdfasdf', 1, '2024-01-02 00:00:00', '2024-02-02 00:00:00', '2024-01-11 15:09:38', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745343819995418625, 1440911410581213417, 'adfasd', 1, '2024-01-06 00:00:00', '2024-02-06 00:00:00', '2024-01-11 15:16:08', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745639100335001600, 1440911410581213417, '1234', 1, '2024-01-12 00:00:00', '2024-01-19 00:00:00', '2024-01-12 10:49:29', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745641568804540417, 1440911410581213417, '123', 1, '2024-01-12 00:00:00', '2024-01-19 00:00:00', '2024-01-12 10:59:17', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1746710995184652289, 1440911410581213417, '11111111111111', 3, '2024-01-16 00:00:00', '2024-01-25 00:00:00', '2024-01-15 09:48:48', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1746821158071701504, 1440911410581213417, 'sfasdf', 1, '2024-02-14 00:00:00', '2024-02-16 00:00:00', '2024-01-15 17:06:33', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1747175673463574529, 1440911410581213417, '1111', 1, '2024-01-16 00:00:00', '2024-01-17 00:00:00', '2024-01-16 16:35:16', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784199563469393920, 1779777400603676672, '111', 1, '2024-04-01 00:00:00', '2024-04-04 00:00:00', '2024-04-27 20:34:59', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784202480981118976, 1779777400603676672, '请假', 1, '2024-04-22 00:00:00', '2024-04-24 00:00:00', '2024-04-27 20:46:35', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784211196795162625, 1779777400603676672, '请假三天', 1, '2024-04-02 00:00:00', '2024-04-05 00:00:00', '2024-04-27 21:21:13', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784221100561928192, 1779777400603676672, '请假出去玩', 1, '2024-04-08 00:00:00', '2024-04-15 00:00:00', '2024-04-27 22:00:34', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784556947194777601, 1779777400603676672, '111', 1, '2024-04-03 00:00:00', '2024-04-11 00:00:00', '2024-04-28 20:15:06', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1785508179405180928, 1779777400603676672, '11', 1, '2024-05-08 00:00:00', '2024-05-10 00:00:00', '2024-05-01 11:14:58', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787771104035606528, 1779777400603676672, '111', 1, '2024-05-07 00:00:00', '2024-05-08 00:00:00', '2024-05-07 17:07:01', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787771998559014913, 1779777400603676672, '2222', 1, '2024-05-07 00:00:00', '2024-05-15 00:00:00', '2024-05-07 17:10:34', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787817506019217408, 1779777400603676672, '111', 1, '2024-05-08 00:00:00', '2024-05-16 00:00:00', '2024-05-07 20:11:24', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787852380893614081, 1779777400603676672, '1111', 1, '2024-05-14 00:00:00', '2024-05-08 00:00:00', '2024-05-07 22:29:59', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787853112791273472, 1779777400603676672, '1111', 1, '2024-05-08 00:00:00', '2024-05-16 00:00:00', '2024-05-07 22:32:53', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788107566534889472, 1779777400603676672, '111', 1, '2024-05-08 00:00:00', '2024-05-09 00:00:00', '2024-05-08 15:24:00', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788112135096635392, 1779777400603676672, '111', 1, '2024-05-08 00:00:00', '2024-05-09 00:00:00', '2024-05-08 15:42:09', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788112525678612480, 1779777400603676672, '1111', 2, '2024-05-09 00:00:00', '2024-05-10 00:00:00', '2024-05-08 15:43:42', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788741582820741120, 1779777400603676672, '秀', 2, '2024-05-07 00:00:00', '2024-05-08 00:00:00', '2024-05-10 09:23:21', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1791767263255203841, 1779777400603676672, '1111', 1, '2024-05-20 00:00:00', '2024-05-21 00:00:00', '2024-05-18 17:46:20', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1792492440158998528, 1779777400603676672, '111222', 2, '2024-05-07 00:00:00', '2024-05-22 00:00:00', '2024-05-20 17:47:55', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1792829634757267456, 1779777400603676672, '1111', 2, '2024-05-14 00:00:00', '2024-05-15 00:00:00', '2024-05-21 16:07:49', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1793489840575090688, 1779777400603676672, '1111', 1, '2024-05-16 00:00:00', '2024-05-24 00:00:00', '2024-05-23 11:51:14', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1795696352311644160, 1779777400603676672, 'dd', 1, '2024-05-02 00:00:00', '2024-05-10 00:00:00', '2024-05-29 13:59:07', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1795992839696420865, 1779777400603676672, 'admin', 1, '2024-05-02 00:00:00', '2024-05-18 00:00:00', '2024-05-30 09:37:16', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1795994391077195776, 1779777400603676672, '1111222', 1, '2024-05-15 00:00:00', '2024-05-16 00:00:00', '2024-05-30 09:43:25', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796109769098924033, 1779777400603676672, '1111', 1, '2024-05-08 00:00:00', '2024-05-10 00:00:00', '2024-05-30 17:21:54', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796110123517612032, 1779777400603676672, '1111222', 1, '2024-05-16 00:00:00', '2024-05-18 00:00:00', '2024-05-30 17:23:18', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796164077765005312, 1779777400603676672, 'admin', 1, '2024-05-10 00:00:00', '2024-06-05 00:00:00', '2024-05-30 20:57:42', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796164941607079936, 1779777400603676672, 'admin', 1, '2024-05-17 00:00:00', '2024-06-12 00:00:00', '2024-05-30 21:01:08', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796173926594777088, 1779777400603676672, 'dd', 1, '2024-05-10 00:00:00', '2024-05-09 00:00:00', '2024-05-30 21:36:50', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796178444468359168, 1779777400603676672, 'x', 1, '2024-05-14 00:00:00', '2024-05-15 00:00:00', '2024-05-30 21:54:47', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796181839363182593, 1779777400603676672, '111', 1, '2024-05-16 00:00:00', '2024-06-18 00:00:00', '2024-05-30 22:08:17', NULL, 4, 'admin'); +INSERT INTO `zz_test_flow_leave` VALUES (1796182559164469249, 1779777400603676672, '4444', 1, '2024-05-08 00:00:00', '2024-05-10 00:00:00', '2024-05-30 22:11:08', NULL, 4, 'admin'); +INSERT INTO `zz_test_flow_leave` VALUES (1796183035536740352, 1779777400603676672, 'dd', 1, '2024-05-18 00:00:00', '2024-05-11 00:00:00', '2024-05-30 22:13:02', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796183248754184192, 1779777400603676672, '11', 1, '2024-05-07 00:00:00', '2024-05-08 00:00:00', '2024-05-30 22:13:53', NULL, 5, 'userTJ2'); +INSERT INTO `zz_test_flow_leave` VALUES (1796185777676226560, 1779777400603676672, 'd', 1, '2024-05-09 00:00:00', '2024-05-09 00:00:00', '2024-05-30 22:23:56', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796187020805017600, 1779777400603676672, 'd', 1, '2024-05-03 00:00:00', '2024-05-03 00:00:00', '2024-05-30 22:28:52', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796188059113361408, 1779777400603676672, 'd', 1, '2024-05-17 00:00:00', '2024-05-17 00:00:00', '2024-05-30 22:33:00', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796188876033757184, 1779777400603676672, 'dd', 1, '2024-05-02 00:00:00', '2024-05-02 00:00:00', '2024-05-30 22:36:14', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796189604152348672, 1779777400603676672, 'dd', 1, '2024-05-03 00:00:00', '2024-05-03 00:00:00', '2024-05-30 22:39:08', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796190467956674560, 1779777400603676672, 'dd', 1, '2024-05-10 00:00:00', '2024-05-16 00:00:00', '2024-05-30 22:42:34', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796191454335340544, 1779777400603676672, 'jk', 1, '2024-05-10 00:00:00', '2024-05-02 00:00:00', '2024-05-30 22:46:29', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796192461547114496, 1779777400603676672, 'd', 1, '2024-05-02 00:00:00', '2024-05-09 00:00:00', '2024-05-30 22:50:29', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796195394187694080, 1779777400603676672, 'dd', 1, '2024-05-03 00:00:00', '2024-05-10 00:00:00', '2024-05-30 23:02:08', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796197180806008832, 1779777400603676672, 'ddd', 1, '2024-05-10 00:00:00', '2024-05-10 00:00:00', '2024-05-30 23:09:14', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796201309611757568, 1779777400603676672, 'dd', 1, '2024-05-17 00:00:00', '2024-05-17 00:00:00', '2024-05-30 23:25:39', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796202010052136960, 1779777400603676672, 'dd', 1, '2024-05-03 00:00:00', '2024-05-03 00:00:00', '2024-05-30 23:28:26', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796204072726958080, 1779777400603676672, 'd', 1, '2024-05-10 00:00:00', '2024-05-10 00:00:00', '2024-05-30 23:36:37', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796354567839944704, 1779777400603676672, 'admin', 1, '2024-05-17 00:00:00', '2024-06-13 00:00:00', '2024-05-31 09:34:38', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796361013583417344, 1779777400603676672, 'admin', 1, '2024-05-11 00:00:00', '2024-06-10 00:00:00', '2024-05-31 10:00:15', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796442194475749377, 1779777400603676672, 'd', 1, '2024-05-10 00:00:00', '2024-06-06 00:00:00', '2024-05-31 15:22:50', NULL, 4, 'admin'); +INSERT INTO `zz_test_flow_leave` VALUES (1796453212681670656, 1779777400603676672, 'admin', 1, '2024-05-18 00:00:00', '2024-06-10 00:00:00', '2024-05-31 16:06:37', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1797540170921152512, 1779777400603676672, '111', 1, '2024-06-04 00:00:00', '2024-06-06 00:00:00', '2024-06-03 16:05:48', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1799012020255723520, 1779777400603676672, '1111', 1, '2024-06-12 00:00:00', '2024-06-14 00:00:00', '2024-06-07 17:34:24', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1800522684333821952, 1779777400603676672, '111', 1, '2024-06-12 00:00:00', '2024-06-12 00:00:00', '2024-06-11 21:37:15', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1800529322327412736, 1799417106157015040, '1111', 1, '2024-06-13 00:00:00', '2024-06-19 00:00:00', '2024-06-11 22:03:37', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1807764854329577473, 1779777400603676672, '111', 1, '2024-07-02 00:00:00', '2024-07-11 00:00:00', '2024-07-01 21:15:03', 11, 1, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1809146480452177920, 1808020007993479168, '111', 1, '2024-07-05 00:00:00', '2024-07-08 00:00:00', '2024-07-05 16:45:08', NULL, NULL, NULL); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/.DS_Store b/OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..95224700c5936d061c02cf8bc1694d09b1bd8063 GIT binary patch literal 6148 zcmeHKJ5Iwu5Pch57(tN|1ck4V8<@zPAQvEsQ9!cg2m`fSjzhx{xCsTf;LYyH!k2+@shnR@PlugAm~Fb0f)2^r8` zCq9|5rj;@VjDf$wfb0(mRWMa-0{X3kgKq(d5zSsWmtI1062(-p2}lpcg;Zilb=qRM zkWPD|aj9YxFr>q2^Wn6z(+b!sA!{O3^HW~xQK$n4|a5 /etc/timezone + +# Ubuntu软件源选择中国的服务器 +RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/services/redis/redis.conf b/OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/services/redis/redis.conf new file mode 100644 index 00000000..2eecfa5a --- /dev/null +++ b/OrangeFormsOpen-MybatisFlex/zz-resource/docker-files/services/redis/redis.conf @@ -0,0 +1,1307 @@ +# Redis configuration file example. +# +# Note that in order to read the configuration file, Redis must be +# started with the file path as first argument: +# +# ./redis-server /path/to/redis.conf + +# Note on units: when memory size is needed, it is possible to specify +# it in the usual form of 1k 5GB 4M and so forth: +# +# 1k => 1000 bytes +# 1kb => 1024 bytes +# 1m => 1000000 bytes +# 1mb => 1024*1024 bytes +# 1g => 1000000000 bytes +# 1gb => 1024*1024*1024 bytes +# +# units are case insensitive so 1GB 1Gb 1gB are all the same. + +################################## INCLUDES ################################### + +# Include one or more other config files here. This is useful if you +# have a standard template that goes to all Redis servers but also need +# to customize a few per-server settings. Include files can include +# other files, so use this wisely. +# +# Notice option "include" won't be rewritten by command "CONFIG REWRITE" +# from admin or Redis Sentinel. Since Redis always uses the last processed +# line as value of a configuration directive, you'd better put includes +# at the beginning of this file to avoid overwriting config change at runtime. +# +# If instead you are interested in using includes to override configuration +# options, it is better to use include as the last line. +# +# include /path/to/local.conf +# include /path/to/other.conf + +################################## MODULES ##################################### + +# Load modules at startup. If the server is not able to load modules +# it will abort. It is possible to use multiple loadmodule directives. +# +# loadmodule /path/to/my_module.so +# loadmodule /path/to/other_module.so + +################################## NETWORK ##################################### + +# By default, if no "bind" configuration directive is specified, Redis listens +# for connections from all the network interfaces available on the server. +# It is possible to listen to just one or multiple selected interfaces using +# the "bind" configuration directive, followed by one or more IP addresses. +# +# Examples: +# +# bind 192.168.1.100 10.0.0.1 +# bind 127.0.0.1 ::1 +# +# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the +# internet, binding to all the interfaces is dangerous and will expose the +# instance to everybody on the internet. So by default we uncomment the +# following bind directive, that will force Redis to listen only into +# the IPv4 lookback interface address (this means Redis will be able to +# accept connections only from clients running into the same computer it +# is running). +# +# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES +# JUST COMMENT THE FOLLOWING LINE. +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +bind 0.0.0.0 + +# Protected mode is a layer of security protection, in order to avoid that +# Redis instances left open on the internet are accessed and exploited. +# +# When protected mode is on and if: +# +# 1) The server is not binding explicitly to a set of addresses using the +# "bind" directive. +# 2) No password is configured. +# +# The server only accepts connections from clients connecting from the +# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain +# sockets. +# +# By default protected mode is enabled. You should disable it only if +# you are sure you want clients from other hosts to connect to Redis +# even if no authentication is configured, nor a specific set of interfaces +# are explicitly listed using the "bind" directive. +protected-mode yes + +# Accept connections on the specified port, default is 6379 (IANA #815344). +# If port 0 is specified Redis will not listen on a TCP socket. +port 6379 + +# TCP listen() backlog. +# +# In high requests-per-second environments you need an high backlog in order +# to avoid slow clients connections issues. Note that the Linux kernel +# will silently truncate it to the value of /proc/sys/net/core/somaxconn so +# make sure to raise both the value of somaxconn and tcp_max_syn_backlog +# in order to get the desired effect. +tcp-backlog 511 + +# Unix socket. +# +# Specify the path for the Unix socket that will be used to listen for +# incoming connections. There is no default, so Redis will not listen +# on a unix socket when not specified. +# +# unixsocket /tmp/redis.sock +# unixsocketperm 700 + +# Close the connection after a client is idle for N seconds (0 to disable) +timeout 0 + +# TCP keepalive. +# +# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence +# of communication. This is useful for two reasons: +# +# 1) Detect dead peers. +# 2) Take the connection alive from the point of view of network +# equipment in the middle. +# +# On Linux, the specified value (in seconds) is the period used to send ACKs. +# Note that to close the connection the double of the time is needed. +# On other kernels the period depends on the kernel configuration. +# +# A reasonable value for this option is 300 seconds, which is the new +# Redis default starting with Redis 3.2.1. +tcp-keepalive 300 + +################################# GENERAL ##################################### + +# By default Redis does not run as a daemon. Use 'yes' if you need it. +# Note that Redis will write a pid file in /var/run/redis.pid when daemonized. +daemonize no + +# If you run Redis from upstart or systemd, Redis can interact with your +# supervision tree. Options: +# supervised no - no supervision interaction +# supervised upstart - signal upstart by putting Redis into SIGSTOP mode +# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET +# supervised auto - detect upstart or systemd method based on +# UPSTART_JOB or NOTIFY_SOCKET environment variables +# Note: these supervision methods only signal "process is ready." +# They do not enable continuous liveness pings back to your supervisor. +supervised no + +# If a pid file is specified, Redis writes it where specified at startup +# and removes it at exit. +# +# When the server runs non daemonized, no pid file is created if none is +# specified in the configuration. When the server is daemonized, the pid file +# is used even if not specified, defaulting to "/var/run/redis.pid". +# +# Creating a pid file is best effort: if Redis is not able to create it +# nothing bad happens, the server will start and run normally. +pidfile /var/run/redis_6379.pid + +# Specify the server verbosity level. +# This can be one of: +# debug (a lot of information, useful for development/testing) +# verbose (many rarely useful info, but not a mess like the debug level) +# notice (moderately verbose, what you want in production probably) +# warning (only very important / critical messages are logged) +loglevel notice + +# Specify the log file name. Also the empty string can be used to force +# Redis to log on the standard output. Note that if you use standard +# output for logging but daemonize, logs will be sent to /dev/null +logfile /var/log/redis_6379.log + +# To enable logging to the system logger, just set 'syslog-enabled' to yes, +# and optionally update the other syslog parameters to suit your needs. +# syslog-enabled no + +# Specify the syslog identity. +# syslog-ident redis + +# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7. +# syslog-facility local0 + +# Set the number of databases. The default database is DB 0, you can select +# a different one on a per-connection basis using SELECT where +# dbid is a number between 0 and 'databases'-1 +databases 16 + +# By default Redis shows an ASCII art logo only when started to log to the +# standard output and if the standard output is a TTY. Basically this means +# that normally a logo is displayed only in interactive sessions. +# +# However it is possible to force the pre-4.0 behavior and always show a +# ASCII art logo in startup logs by setting the following option to yes. +always-show-logo yes + +################################ SNAPSHOTTING ################################ +# +# Save the DB on disk: +# +# save +# +# Will save the DB if both the given number of seconds and the given +# number of write operations against the DB occurred. +# +# In the example below the behaviour will be to save: +# after 900 sec (15 min) if at least 1 key changed +# after 300 sec (5 min) if at least 10 keys changed +# after 60 sec if at least 10000 keys changed +# +# Note: you can disable saving completely by commenting out all "save" lines. +# +# It is also possible to remove all the previously configured save +# points by adding a save directive with a single empty string argument +# like in the following example: +# +# save "" + +save 900 1 +save 300 10 +save 60 10000 + +# By default Redis will stop accepting writes if RDB snapshots are enabled +# (at least one save point) and the latest background save failed. +# This will make the user aware (in a hard way) that data is not persisting +# on disk properly, otherwise chances are that no one will notice and some +# disaster will happen. +# +# If the background saving process will start working again Redis will +# automatically allow writes again. +# +# However if you have setup your proper monitoring of the Redis server +# and persistence, you may want to disable this feature so that Redis will +# continue to work as usual even if there are problems with disk, +# permissions, and so forth. +stop-writes-on-bgsave-error yes + +# Compress string objects using LZF when dump .rdb databases? +# For default that's set to 'yes' as it's almost always a win. +# If you want to save some CPU in the saving child set it to 'no' but +# the dataset will likely be bigger if you have compressible values or keys. +rdbcompression yes + +# Since version 5 of RDB a CRC64 checksum is placed at the end of the file. +# This makes the format more resistant to corruption but there is a performance +# hit to pay (around 10%) when saving and loading RDB files, so you can disable it +# for maximum performances. +# +# RDB files created with checksum disabled have a checksum of zero that will +# tell the loading code to skip the check. +rdbchecksum yes + +# The filename where to dump the DB +dbfilename dump.rdb + +# The working directory. +# +# The DB will be written inside this directory, with the filename specified +# above using the 'dbfilename' configuration directive. +# +# The Append Only File will also be created inside this directory. +# +# Note that you must specify a directory here, not a file name. +dir ./ + +################################# REPLICATION ################################# + +# Master-Slave replication. Use slaveof to make a Redis instance a copy of +# another Redis server. A few things to understand ASAP about Redis replication. +# +# 1) Redis replication is asynchronous, but you can configure a master to +# stop accepting writes if it appears to be not connected with at least +# a given number of slaves. +# 2) Redis slaves are able to perform a partial resynchronization with the +# master if the replication link is lost for a relatively small amount of +# time. You may want to configure the replication backlog size (see the next +# sections of this file) with a sensible value depending on your needs. +# 3) Replication is automatic and does not need user intervention. After a +# network partition slaves automatically try to reconnect to masters +# and resynchronize with them. +# +# slaveof + +# If the master is password protected (using the "requirepass" configuration +# directive below) it is possible to tell the slave to authenticate before +# starting the replication synchronization process, otherwise the master will +# refuse the slave request. +# +# masterauth + +# When a slave loses its connection with the master, or when the replication +# is still in progress, the slave can act in two different ways: +# +# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will +# still reply to client requests, possibly with out of date data, or the +# data set may just be empty if this is the first synchronization. +# +# 2) if slave-serve-stale-data is set to 'no' the slave will reply with +# an error "SYNC with master in progress" to all the kind of commands +# but to INFO and SLAVEOF. +# +slave-serve-stale-data yes + +# You can configure a slave instance to accept writes or not. Writing against +# a slave instance may be useful to store some ephemeral data (because data +# written on a slave will be easily deleted after resync with the master) but +# may also cause problems if clients are writing to it because of a +# misconfiguration. +# +# Since Redis 2.6 by default slaves are read-only. +# +# Note: read only slaves are not designed to be exposed to untrusted clients +# on the internet. It's just a protection layer against misuse of the instance. +# Still a read only slave exports by default all the administrative commands +# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve +# security of read only slaves using 'rename-command' to shadow all the +# administrative / dangerous commands. +slave-read-only yes + +# Replication SYNC strategy: disk or socket. +# +# ------------------------------------------------------- +# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY +# ------------------------------------------------------- +# +# New slaves and reconnecting slaves that are not able to continue the replication +# process just receiving differences, need to do what is called a "full +# synchronization". An RDB file is transmitted from the master to the slaves. +# The transmission can happen in two different ways: +# +# 1) Disk-backed: The Redis master creates a new process that writes the RDB +# file on disk. Later the file is transferred by the parent +# process to the slaves incrementally. +# 2) Diskless: The Redis master creates a new process that directly writes the +# RDB file to slave sockets, without touching the disk at all. +# +# With disk-backed replication, while the RDB file is generated, more slaves +# can be queued and served with the RDB file as soon as the current child producing +# the RDB file finishes its work. With diskless replication instead once +# the transfer starts, new slaves arriving will be queued and a new transfer +# will start when the current one terminates. +# +# When diskless replication is used, the master waits a configurable amount of +# time (in seconds) before starting the transfer in the hope that multiple slaves +# will arrive and the transfer can be parallelized. +# +# With slow disks and fast (large bandwidth) networks, diskless replication +# works better. +repl-diskless-sync no + +# When diskless replication is enabled, it is possible to configure the delay +# the server waits in order to spawn the child that transfers the RDB via socket +# to the slaves. +# +# This is important since once the transfer starts, it is not possible to serve +# new slaves arriving, that will be queued for the next RDB transfer, so the server +# waits a delay in order to let more slaves arrive. +# +# The delay is specified in seconds, and by default is 5 seconds. To disable +# it entirely just set it to 0 seconds and the transfer will start ASAP. +repl-diskless-sync-delay 5 + +# Slaves send PINGs to server in a predefined interval. It's possible to change +# this interval with the repl_ping_slave_period option. The default value is 10 +# seconds. +# +# repl-ping-slave-period 10 + +# The following option sets the replication timeout for: +# +# 1) Bulk transfer I/O during SYNC, from the point of view of slave. +# 2) Master timeout from the point of view of slaves (data, pings). +# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings). +# +# It is important to make sure that this value is greater than the value +# specified for repl-ping-slave-period otherwise a timeout will be detected +# every time there is low traffic between the master and the slave. +# +# repl-timeout 60 + +# Disable TCP_NODELAY on the slave socket after SYNC? +# +# If you select "yes" Redis will use a smaller number of TCP packets and +# less bandwidth to send data to slaves. But this can add a delay for +# the data to appear on the slave side, up to 40 milliseconds with +# Linux kernels using a default configuration. +# +# If you select "no" the delay for data to appear on the slave side will +# be reduced but more bandwidth will be used for replication. +# +# By default we optimize for low latency, but in very high traffic conditions +# or when the master and slaves are many hops away, turning this to "yes" may +# be a good idea. +repl-disable-tcp-nodelay no + +# Set the replication backlog size. The backlog is a buffer that accumulates +# slave data when slaves are disconnected for some time, so that when a slave +# wants to reconnect again, often a full resync is not needed, but a partial +# resync is enough, just passing the portion of data the slave missed while +# disconnected. +# +# The bigger the replication backlog, the longer the time the slave can be +# disconnected and later be able to perform a partial resynchronization. +# +# The backlog is only allocated once there is at least a slave connected. +# +# repl-backlog-size 1mb + +# After a master has no longer connected slaves for some time, the backlog +# will be freed. The following option configures the amount of seconds that +# need to elapse, starting from the time the last slave disconnected, for +# the backlog buffer to be freed. +# +# Note that slaves never free the backlog for timeout, since they may be +# promoted to masters later, and should be able to correctly "partially +# resynchronize" with the slaves: hence they should always accumulate backlog. +# +# A value of 0 means to never release the backlog. +# +# repl-backlog-ttl 3600 + +# The slave priority is an integer number published by Redis in the INFO output. +# It is used by Redis Sentinel in order to select a slave to promote into a +# master if the master is no longer working correctly. +# +# A slave with a low priority number is considered better for promotion, so +# for instance if there are three slaves with priority 10, 100, 25 Sentinel will +# pick the one with priority 10, that is the lowest. +# +# However a special priority of 0 marks the slave as not able to perform the +# role of master, so a slave with priority of 0 will never be selected by +# Redis Sentinel for promotion. +# +# By default the priority is 100. +slave-priority 100 + +# It is possible for a master to stop accepting writes if there are less than +# N slaves connected, having a lag less or equal than M seconds. +# +# The N slaves need to be in "online" state. +# +# The lag in seconds, that must be <= the specified value, is calculated from +# the last ping received from the slave, that is usually sent every second. +# +# This option does not GUARANTEE that N replicas will accept the write, but +# will limit the window of exposure for lost writes in case not enough slaves +# are available, to the specified number of seconds. +# +# For example to require at least 3 slaves with a lag <= 10 seconds use: +# +# min-slaves-to-write 3 +# min-slaves-max-lag 10 +# +# Setting one or the other to 0 disables the feature. +# +# By default min-slaves-to-write is set to 0 (feature disabled) and +# min-slaves-max-lag is set to 10. + +# A Redis master is able to list the address and port of the attached +# slaves in different ways. For example the "INFO replication" section +# offers this information, which is used, among other tools, by +# Redis Sentinel in order to discover slave instances. +# Another place where this info is available is in the output of the +# "ROLE" command of a master. +# +# The listed IP and address normally reported by a slave is obtained +# in the following way: +# +# IP: The address is auto detected by checking the peer address +# of the socket used by the slave to connect with the master. +# +# Port: The port is communicated by the slave during the replication +# handshake, and is normally the port that the slave is using to +# list for connections. +# +# However when port forwarding or Network Address Translation (NAT) is +# used, the slave may be actually reachable via different IP and port +# pairs. The following two options can be used by a slave in order to +# report to its master a specific set of IP and port, so that both INFO +# and ROLE will report those values. +# +# There is no need to use both the options if you need to override just +# the port or the IP address. +# +# slave-announce-ip 5.5.5.5 +# slave-announce-port 1234 + +################################## SECURITY ################################### + +# Require clients to issue AUTH before processing any other +# commands. This might be useful in environments in which you do not trust +# others with access to the host running redis-server. +# +# This should stay commented out for backward compatibility and because most +# people do not need auth (e.g. they run their own servers). +# +# Warning: since Redis is pretty fast an outside user can try up to +# 150k passwords per second against a good box. This means that you should +# use a very strong password otherwise it will be very easy to break. +# +# requirepass foobared + +# Command renaming. +# +# It is possible to change the name of dangerous commands in a shared +# environment. For instance the CONFIG command may be renamed into something +# hard to guess so that it will still be available for internal-use tools +# but not available for general clients. +# +# Example: +# +# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 +# +# It is also possible to completely kill a command by renaming it into +# an empty string: +# +# rename-command CONFIG "" +# +# Please note that changing the name of commands that are logged into the +# AOF file or transmitted to slaves may cause problems. + +################################### CLIENTS #################################### + +# Set the max number of connected clients at the same time. By default +# this limit is set to 10000 clients, however if the Redis server is not +# able to configure the process file limit to allow for the specified limit +# the max number of allowed clients is set to the current file limit +# minus 32 (as Redis reserves a few file descriptors for internal uses). +# +# Once the limit is reached Redis will close all the new connections sending +# an error 'max number of clients reached'. +# +# maxclients 10000 + +############################## MEMORY MANAGEMENT ################################ + +# Set a memory usage limit to the specified amount of bytes. +# When the memory limit is reached Redis will try to remove keys +# according to the eviction policy selected (see maxmemory-policy). +# +# If Redis can't remove keys according to the policy, or if the policy is +# set to 'noeviction', Redis will start to reply with errors to commands +# that would use more memory, like SET, LPUSH, and so on, and will continue +# to reply to read-only commands like GET. +# +# This option is usually useful when using Redis as an LRU or LFU cache, or to +# set a hard memory limit for an instance (using the 'noeviction' policy). +# +# WARNING: If you have slaves attached to an instance with maxmemory on, +# the size of the output buffers needed to feed the slaves are subtracted +# from the used memory count, so that network problems / resyncs will +# not trigger a loop where keys are evicted, and in turn the output +# buffer of slaves is full with DELs of keys evicted triggering the deletion +# of more keys, and so forth until the database is completely emptied. +# +# In short... if you have slaves attached it is suggested that you set a lower +# limit for maxmemory so that there is some free RAM on the system for slave +# output buffers (but this is not needed if the policy is 'noeviction'). +# +# maxmemory + +# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory +# is reached. You can select among five behaviors: +# +# volatile-lru -> Evict using approximated LRU among the keys with an expire set. +# allkeys-lru -> Evict any key using approximated LRU. +# volatile-lfu -> Evict using approximated LFU among the keys with an expire set. +# allkeys-lfu -> Evict any key using approximated LFU. +# volatile-random -> Remove a random key among the ones with an expire set. +# allkeys-random -> Remove a random key, any key. +# volatile-ttl -> Remove the key with the nearest expire time (minor TTL) +# noeviction -> Don't evict anything, just return an error on write operations. +# +# LRU means Least Recently Used +# LFU means Least Frequently Used +# +# Both LRU, LFU and volatile-ttl are implemented using approximated +# randomized algorithms. +# +# Note: with any of the above policies, Redis will return an error on write +# operations, when there are no suitable keys for eviction. +# +# At the date of writing these commands are: set setnx setex append +# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd +# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby +# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby +# getset mset msetnx exec sort +# +# The default is: +# +# maxmemory-policy noeviction + +# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated +# algorithms (in order to save memory), so you can tune it for speed or +# accuracy. For default Redis will check five keys and pick the one that was +# used less recently, you can change the sample size using the following +# configuration directive. +# +# The default of 5 produces good enough results. 10 Approximates very closely +# true LRU but costs more CPU. 3 is faster but not very accurate. +# +# maxmemory-samples 5 + +############################# LAZY FREEING #################################### + +# Redis has two primitives to delete keys. One is called DEL and is a blocking +# deletion of the object. It means that the server stops processing new commands +# in order to reclaim all the memory associated with an object in a synchronous +# way. If the key deleted is associated with a small object, the time needed +# in order to execute the DEL command is very small and comparable to most other +# O(1) or O(log_N) commands in Redis. However if the key is associated with an +# aggregated value containing millions of elements, the server can block for +# a long time (even seconds) in order to complete the operation. +# +# For the above reasons Redis also offers non blocking deletion primitives +# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and +# FLUSHDB commands, in order to reclaim memory in background. Those commands +# are executed in constant time. Another thread will incrementally free the +# object in the background as fast as possible. +# +# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled. +# It's up to the design of the application to understand when it is a good +# idea to use one or the other. However the Redis server sometimes has to +# delete keys or flush the whole database as a side effect of other operations. +# Specifically Redis deletes objects independently of a user call in the +# following scenarios: +# +# 1) On eviction, because of the maxmemory and maxmemory policy configurations, +# in order to make room for new data, without going over the specified +# memory limit. +# 2) Because of expire: when a key with an associated time to live (see the +# EXPIRE command) must be deleted from memory. +# 3) Because of a side effect of a command that stores data on a key that may +# already exist. For example the RENAME command may delete the old key +# content when it is replaced with another one. Similarly SUNIONSTORE +# or SORT with STORE option may delete existing keys. The SET command +# itself removes any old content of the specified key in order to replace +# it with the specified string. +# 4) During replication, when a slave performs a full resynchronization with +# its master, the content of the whole database is removed in order to +# load the RDB file just transfered. +# +# In all the above cases the default is to delete objects in a blocking way, +# like if DEL was called. However you can configure each case specifically +# in order to instead release memory in a non-blocking way like if UNLINK +# was called, using the following configuration directives: + +lazyfree-lazy-eviction no +lazyfree-lazy-expire no +lazyfree-lazy-server-del no +slave-lazy-flush no + +############################## APPEND ONLY MODE ############################### + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good enough in many applications, but an issue with the Redis process or +# a power outage may result into a few minutes of writes lost (depending on +# the configured save points). +# +# The Append Only File is an alternative persistence mode that provides +# much better durability. For instance using the default data fsync policy +# (see later in the config file) Redis can lose just one second of writes in a +# dramatic event like a server power outage, or a single write if something +# wrong with the Redis process itself happens, but the operating system is +# still running correctly. +# +# AOF and RDB persistence can be enabled at the same time without problems. +# If the AOF is enabled on startup Redis will load the AOF, that is the file +# with the better durability guarantees. +# +# Please check http://redis.io/topics/persistence for more information. + +appendonly no + +# The name of the append only file (default: "appendonly.aof") + +appendfilename "appendonly.aof" + +# The fsync() call tells the Operating System to actually write data on disk +# instead of waiting for more data in the output buffer. Some OS will really flush +# data on disk, some other OS will just try to do it ASAP. +# +# Redis supports three different modes: +# +# no: don't fsync, just let the OS flush the data when it wants. Faster. +# always: fsync after every write to the append only log. Slow, Safest. +# everysec: fsync only one time every second. Compromise. +# +# The default is "everysec", as that's usually the right compromise between +# speed and data safety. It's up to you to understand if you can relax this to +# "no" that will let the operating system flush the output buffer when +# it wants, for better performances (but if you can live with the idea of +# some data loss consider the default persistence mode that's snapshotting), +# or on the contrary, use "always" that's very slow but a bit safer than +# everysec. +# +# More details please check the following article: +# http://antirez.com/post/redis-persistence-demystified.html +# +# If unsure, use "everysec". + +# appendfsync always +appendfsync everysec +# appendfsync no + +# When the AOF fsync policy is set to always or everysec, and a background +# saving process (a background save or AOF log background rewriting) is +# performing a lot of I/O against the disk, in some Linux configurations +# Redis may block too long on the fsync() call. Note that there is no fix for +# this currently, as even performing fsync in a different thread will block +# our synchronous write(2) call. +# +# In order to mitigate this problem it's possible to use the following option +# that will prevent fsync() from being called in the main process while a +# BGSAVE or BGREWRITEAOF is in progress. +# +# This means that while another child is saving, the durability of Redis is +# the same as "appendfsync none". In practical terms, this means that it is +# possible to lose up to 30 seconds of log in the worst scenario (with the +# default Linux settings). +# +# If you have latency problems turn this to "yes". Otherwise leave it as +# "no" that is the safest pick from the point of view of durability. + +no-appendfsync-on-rewrite no + +# Automatic rewrite of the append only file. +# Redis is able to automatically rewrite the log file implicitly calling +# BGREWRITEAOF when the AOF log size grows by the specified percentage. +# +# This is how it works: Redis remembers the size of the AOF file after the +# latest rewrite (if no rewrite has happened since the restart, the size of +# the AOF at startup is used). +# +# This base size is compared to the current size. If the current size is +# bigger than the specified percentage, the rewrite is triggered. Also +# you need to specify a minimal size for the AOF file to be rewritten, this +# is useful to avoid rewriting the AOF file even if the percentage increase +# is reached but it is still pretty small. +# +# Specify a percentage of zero in order to disable the automatic AOF +# rewrite feature. + +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb + +# An AOF file may be found to be truncated at the end during the Redis +# startup process, when the AOF data gets loaded back into memory. +# This may happen when the system where Redis is running +# crashes, especially when an ext4 filesystem is mounted without the +# data=ordered option (however this can't happen when Redis itself +# crashes or aborts but the operating system still works correctly). +# +# Redis can either exit with an error when this happens, or load as much +# data as possible (the default now) and start if the AOF file is found +# to be truncated at the end. The following option controls this behavior. +# +# If aof-load-truncated is set to yes, a truncated AOF file is loaded and +# the Redis server starts emitting a log to inform the user of the event. +# Otherwise if the option is set to no, the server aborts with an error +# and refuses to start. When the option is set to no, the user requires +# to fix the AOF file using the "redis-check-aof" utility before to restart +# the server. +# +# Note that if the AOF file will be found to be corrupted in the middle +# the server will still exit with an error. This option only applies when +# Redis will try to read more data from the AOF file but not enough bytes +# will be found. +aof-load-truncated yes + +# When rewriting the AOF file, Redis is able to use an RDB preamble in the +# AOF file for faster rewrites and recoveries. When this option is turned +# on the rewritten AOF file is composed of two different stanzas: +# +# [RDB file][AOF tail] +# +# When loading Redis recognizes that the AOF file starts with the "REDIS" +# string and loads the prefixed RDB file, and continues loading the AOF +# tail. +# +# This is currently turned off by default in order to avoid the surprise +# of a format change, but will at some point be used as the default. +aof-use-rdb-preamble no + +################################ LUA SCRIPTING ############################### + +# Max execution time of a Lua script in milliseconds. +# +# If the maximum execution time is reached Redis will log that a script is +# still in execution after the maximum allowed time and will start to +# reply to queries with an error. +# +# When a long running script exceeds the maximum execution time only the +# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be +# used to stop a script that did not yet called write commands. The second +# is the only way to shut down the server in the case a write command was +# already issued by the script but the user doesn't want to wait for the natural +# termination of the script. +# +# Set it to 0 or a negative value for unlimited execution without warnings. +lua-time-limit 5000 + +################################ REDIS CLUSTER ############################### +# +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however +# in order to mark it as "mature" we need to wait for a non trivial percentage +# of users to deploy it in production. +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# Normal Redis instances can't be part of a Redis Cluster; only nodes that are +# started as cluster nodes can. In order to start a Redis instance as a +# cluster node enable the cluster support uncommenting the following: +# +# cluster-enabled yes + +# Every cluster node has a cluster configuration file. This file is not +# intended to be edited by hand. It is created and updated by Redis nodes. +# Every Redis Cluster node requires a different cluster configuration file. +# Make sure that instances running in the same system do not have +# overlapping cluster configuration file names. +# +# cluster-config-file nodes-6379.conf + +# Cluster node timeout is the amount of milliseconds a node must be unreachable +# for it to be considered in failure state. +# Most other internal time limits are multiple of the node timeout. +# +# cluster-node-timeout 15000 + +# A slave of a failing master will avoid to start a failover if its data +# looks too old. +# +# There is no simple way for a slave to actually have an exact measure of +# its "data age", so the following two checks are performed: +# +# 1) If there are multiple slaves able to failover, they exchange messages +# in order to try to give an advantage to the slave with the best +# replication offset (more data from the master processed). +# Slaves will try to get their rank by offset, and apply to the start +# of the failover a delay proportional to their rank. +# +# 2) Every single slave computes the time of the last interaction with +# its master. This can be the last ping or command received (if the master +# is still in the "connected" state), or the time that elapsed since the +# disconnection with the master (if the replication link is currently down). +# If the last interaction is too old, the slave will not try to failover +# at all. +# +# The point "2" can be tuned by user. Specifically a slave will not perform +# the failover if, since the last interaction with the master, the time +# elapsed is greater than: +# +# (node-timeout * slave-validity-factor) + repl-ping-slave-period +# +# So for example if node-timeout is 30 seconds, and the slave-validity-factor +# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the +# slave will not try to failover if it was not able to talk with the master +# for longer than 310 seconds. +# +# A large slave-validity-factor may allow slaves with too old data to failover +# a master, while a too small value may prevent the cluster from being able to +# elect a slave at all. +# +# For maximum availability, it is possible to set the slave-validity-factor +# to a value of 0, which means, that slaves will always try to failover the +# master regardless of the last time they interacted with the master. +# (However they'll always try to apply a delay proportional to their +# offset rank). +# +# Zero is the only value able to guarantee that when all the partitions heal +# the cluster will always be able to continue. +# +# cluster-slave-validity-factor 10 + +# Cluster slaves are able to migrate to orphaned masters, that are masters +# that are left without working slaves. This improves the cluster ability +# to resist to failures as otherwise an orphaned master can't be failed over +# in case of failure if it has no working slaves. +# +# Slaves migrate to orphaned masters only if there are still at least a +# given number of other working slaves for their old master. This number +# is the "migration barrier". A migration barrier of 1 means that a slave +# will migrate only if there is at least 1 other working slave for its master +# and so forth. It usually reflects the number of slaves you want for every +# master in your cluster. +# +# Default is 1 (slaves migrate only if their masters remain with at least +# one slave). To disable migration just set it to a very large value. +# A value of 0 can be set but is useful only for debugging and dangerous +# in production. +# +# cluster-migration-barrier 1 + +# By default Redis Cluster nodes stop accepting queries if they detect there +# is at least an hash slot uncovered (no available node is serving it). +# This way if the cluster is partially down (for example a range of hash slots +# are no longer covered) all the cluster becomes, eventually, unavailable. +# It automatically returns available as soon as all the slots are covered again. +# +# However sometimes you want the subset of the cluster which is working, +# to continue to accept queries for the part of the key space that is still +# covered. In order to do so, just set the cluster-require-full-coverage +# option to no. +# +# cluster-require-full-coverage yes + +# In order to setup your cluster make sure to read the documentation +# available at http://redis.io web site. + +########################## CLUSTER DOCKER/NAT support ######################## + +# In certain deployments, Redis Cluster nodes address discovery fails, because +# addresses are NAT-ted or because ports are forwarded (the typical case is +# Docker and other containers). +# +# In order to make Redis Cluster working in such environments, a static +# configuration where each node knows its public address is needed. The +# following two options are used for this scope, and are: +# +# * cluster-announce-ip +# * cluster-announce-port +# * cluster-announce-bus-port +# +# Each instruct the node about its address, client port, and cluster message +# bus port. The information is then published in the header of the bus packets +# so that other nodes will be able to correctly map the address of the node +# publishing the information. +# +# If the above options are not used, the normal Redis Cluster auto-detection +# will be used instead. +# +# Note that when remapped, the bus port may not be at the fixed offset of +# clients port + 10000, so you can specify any port and bus-port depending +# on how they get remapped. If the bus-port is not set, a fixed offset of +# 10000 will be used as usually. +# +# Example: +# +# cluster-announce-ip 10.1.1.5 +# cluster-announce-port 6379 +# cluster-announce-bus-port 6380 + +################################## SLOW LOG ################################### + +# The Redis Slow Log is a system to log queries that exceeded a specified +# execution time. The execution time does not include the I/O operations +# like talking with the client, sending the reply and so forth, +# but just the time needed to actually execute the command (this is the only +# stage of command execution where the thread is blocked and can not serve +# other requests in the meantime). +# +# You can configure the slow log with two parameters: one tells Redis +# what is the execution time, in microseconds, to exceed in order for the +# command to get logged, and the other parameter is the length of the +# slow log. When a new command is logged the oldest one is removed from the +# queue of logged commands. + +# The following time is expressed in microseconds, so 1000000 is equivalent +# to one second. Note that a negative number disables the slow log, while +# a value of zero forces the logging of every command. +slowlog-log-slower-than 10000 + +# There is no limit to this length. Just be aware that it will consume memory. +# You can reclaim memory used by the slow log with SLOWLOG RESET. +slowlog-max-len 128 + +################################ LATENCY MONITOR ############################## + +# The Redis latency monitoring subsystem samples different operations +# at runtime in order to collect data related to possible sources of +# latency of a Redis instance. +# +# Via the LATENCY command this information is available to the user that can +# print graphs and obtain reports. +# +# The system only logs operations that were performed in a time equal or +# greater than the amount of milliseconds specified via the +# latency-monitor-threshold configuration directive. When its value is set +# to zero, the latency monitor is turned off. +# +# By default latency monitoring is disabled since it is mostly not needed +# if you don't have latency issues, and collecting data has a performance +# impact, that while very small, can be measured under big load. Latency +# monitoring can easily be enabled at runtime using the command +# "CONFIG SET latency-monitor-threshold " if needed. +latency-monitor-threshold 0 + +############################# EVENT NOTIFICATION ############################## + +# Redis can notify Pub/Sub clients about events happening in the key space. +# This feature is documented at http://redis.io/topics/notifications +# +# For instance if keyspace events notification is enabled, and a client +# performs a DEL operation on key "foo" stored in the Database 0, two +# messages will be published via Pub/Sub: +# +# PUBLISH __keyspace@0__:foo del +# PUBLISH __keyevent@0__:del foo +# +# It is possible to select the events that Redis will notify among a set +# of classes. Every class is identified by a single character: +# +# K Keyspace events, published with __keyspace@__ prefix. +# E Keyevent events, published with __keyevent@__ prefix. +# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ... +# $ String commands +# l List commands +# s Set commands +# h Hash commands +# z Sorted set commands +# x Expired events (events generated every time a key expires) +# e Evicted events (events generated when a key is evicted for maxmemory) +# A Alias for g$lshzxe, so that the "AKE" string means all the events. +# +# The "notify-keyspace-events" takes as argument a string that is composed +# of zero or multiple characters. The empty string means that notifications +# are disabled. +# +# Example: to enable list and generic events, from the point of view of the +# event name, use: +# +# notify-keyspace-events Elg +# +# Example 2: to get the stream of the expired keys subscribing to channel +# name __keyevent@0__:expired use: +# +# notify-keyspace-events Ex +# +# By default all notifications are disabled because most users don't need +# this feature and the feature has some overhead. Note that if you don't +# specify at least one of K or E, no events will be delivered. +notify-keyspace-events "" + +############################### ADVANCED CONFIG ############################### + +# Hashes are encoded using a memory efficient data structure when they have a +# small number of entries, and the biggest entry does not exceed a given +# threshold. These thresholds can be configured using the following directives. +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 + +# Lists are also encoded in a special way to save a lot of space. +# The number of entries allowed per internal list node can be specified +# as a fixed maximum size or a maximum number of elements. +# For a fixed maximum size, use -5 through -1, meaning: +# -5: max size: 64 Kb <-- not recommended for normal workloads +# -4: max size: 32 Kb <-- not recommended +# -3: max size: 16 Kb <-- probably not recommended +# -2: max size: 8 Kb <-- good +# -1: max size: 4 Kb <-- good +# Positive numbers mean store up to _exactly_ that number of elements +# per list node. +# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size), +# but if your use case is unique, adjust the settings as necessary. +list-max-ziplist-size -2 + +# Lists may also be compressed. +# Compress depth is the number of quicklist ziplist nodes from *each* side of +# the list to *exclude* from compression. The head and tail of the list +# are always uncompressed for fast push/pop operations. Settings are: +# 0: disable all list compression +# 1: depth 1 means "don't start compressing until after 1 node into the list, +# going from either the head or tail" +# So: [head]->node->node->...->node->[tail] +# [head], [tail] will always be uncompressed; inner nodes will compress. +# 2: [head]->[next]->node->node->...->node->[prev]->[tail] +# 2 here means: don't compress head or head->next or tail->prev or tail, +# but compress all nodes between them. +# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail] +# etc. +list-compress-depth 0 + +# Sets have a special encoding in just one case: when a set is composed +# of just strings that happen to be integers in radix 10 in the range +# of 64 bit signed integers. +# The following configuration setting sets the limit in the size of the +# set in order to use this special memory saving encoding. +set-max-intset-entries 512 + +# Similarly to hashes and lists, sorted sets are also specially encoded in +# order to save a lot of space. This encoding is only used when the length and +# elements of a sorted set are below the following limits: +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 + +# HyperLogLog sparse representation bytes limit. The limit includes the +# 16 bytes header. When an HyperLogLog using the sparse representation crosses +# this limit, it is converted into the dense representation. +# +# A value greater than 16000 is totally useless, since at that point the +# dense representation is more memory efficient. +# +# The suggested value is ~ 3000 in order to have the benefits of +# the space efficient encoding without slowing down too much PFADD, +# which is O(N) with the sparse encoding. The value can be raised to +# ~ 10000 when CPU is not a concern, but space is, and the data set is +# composed of many HyperLogLogs with cardinality in the 0 - 15000 range. +hll-sparse-max-bytes 3000 + +# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in +# order to help rehashing the main Redis hash table (the one mapping top-level +# keys to values). The hash table implementation Redis uses (see dict.c) +# performs a lazy rehashing: the more operation you run into a hash table +# that is rehashing, the more rehashing "steps" are performed, so if the +# server is idle the rehashing is never complete and some more memory is used +# by the hash table. +# +# The default is to use this millisecond 10 times every second in order to +# actively rehash the main dictionaries, freeing memory when possible. +# +# If unsure: +# use "activerehashing no" if you have hard latency requirements and it is +# not a good thing in your environment that Redis can reply from time to time +# to queries with 2 milliseconds delay. +# +# use "activerehashing yes" if you don't have such hard requirements but +# want to free memory asap when possible. +activerehashing yes + +# The client output buffer limits can be used to force disconnection of clients +# that are not reading data from the server fast enough for some reason (a +# common reason is that a Pub/Sub client can't consume messages as fast as the +# publisher can produce them). +# +# The limit can be set differently for the three different classes of clients: +# +# normal -> normal clients including MONITOR clients +# slave -> slave clients +# pubsub -> clients subscribed to at least one pubsub channel or pattern +# +# The syntax of every client-output-buffer-limit directive is the following: +# +# client-output-buffer-limit +# +# A client is immediately disconnected once the hard limit is reached, or if +# the soft limit is reached and remains reached for the specified number of +# seconds (continuously). +# So for instance if the hard limit is 32 megabytes and the soft limit is +# 16 megabytes / 10 seconds, the client will get disconnected immediately +# if the size of the output buffers reach 32 megabytes, but will also get +# disconnected if the client reaches 16 megabytes and continuously overcomes +# the limit for 10 seconds. +# +# By default normal clients are not limited because they don't receive data +# without asking (in a push way), but just after a request, so only +# asynchronous clients may create a scenario where data is requested faster +# than it can read. +# +# Instead there is a default limit for pubsub and slave clients, since +# subscribers and slaves receive data in a push fashion. +# +# Both the hard or the soft limit can be disabled by setting them to zero. +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 + +# Client query buffers accumulate new commands. They are limited to a fixed +# amount by default in order to avoid that a protocol desynchronization (for +# instance due to a bug in the client) will lead to unbound memory usage in +# the query buffer. However you can configure it here if you have very special +# needs, such us huge multi/exec requests or alike. +# +# client-query-buffer-limit 1gb + +# In the Redis protocol, bulk requests, that are, elements representing single +# strings, are normally limited ot 512 mb. However you can change this limit +# here. +# +# proto-max-bulk-len 512mb + +# Redis calls an internal function to perform many background tasks, like +# closing connections of clients in timeout, purging expired keys that are +# never requested, and so forth. +# +# Not all tasks are performed with the same frequency, but Redis checks for +# tasks to perform according to the specified "hz" value. +# +# By default "hz" is set to 10. Raising the value will use more CPU when +# Redis is idle, but at the same time will make Redis more responsive when +# there are many keys expiring at the same time, and timeouts may be +# handled with more precision. +# +# The range is between 1 and 500, however a value over 100 is usually not +# a good idea. Most users should use the default of 10 and raise this up to +# 100 only in environments where very low latency is required. +hz 10 + +# When a child rewrites the AOF file, if the following option is enabled +# the file will be fsync-ed every 32 MB of data generated. This is useful +# in order to commit the file to the disk more incrementally and avoid +# big latency spikes. +aof-rewrite-incremental-fsync yes + +# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good +# idea to start with the default settings and only change them after investigating +# how to improve the performances and how the keys LFU change over time, which +# is possible to inspect via the OBJECT FREQ command. +# +# There are two tunable parameters in the Redis LFU implementation: the +# counter logarithm factor and the counter decay time. It is important to +# understand what the two parameters mean before changing them. +# +# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis +# uses a probabilistic increment with logarithmic behavior. Given the value +# of the old counter, when a key is accessed, the counter is incremented in +# this way: +# +# 1. A random number R between 0 and 1 is extracted. +# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1). +# 3. The counter is incremented only if R < P. +# +# The default lfu-log-factor is 10. This is a table of how the frequency +# counter changes with a different number of accesses with different +# logarithmic factors: +# +# +--------+------------+------------+------------+------------+------------+ +# | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits | +# +--------+------------+------------+------------+------------+------------+ +# | 0 | 104 | 255 | 255 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 1 | 18 | 49 | 255 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 10 | 10 | 18 | 142 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 100 | 8 | 11 | 49 | 143 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# +# NOTE: The above table was obtained by running the following commands: +# +# redis-benchmark -n 1000000 incr foo +# redis-cli object freq foo +# +# NOTE 2: The counter initial value is 5 in order to give new objects a chance +# to accumulate hits. +# +# The counter decay time is the time, in minutes, that must elapse in order +# for the key counter to be divided by two (or decremented if it has a value +# less <= 10). +# +# The default value for the lfu-decay-time is 1. A Special value of 0 means to +# decay the counter every time it happens to be scanned. +# +# lfu-log-factor 10 +# lfu-decay-time 1 + +########################### ACTIVE DEFRAGMENTATION ####################### +# +# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested +# even in production and manually tested by multiple engineers for some +# time. +# +# What is active defragmentation? +# ------------------------------- +# +# Active (online) defragmentation allows a Redis server to compact the +# spaces left between small allocations and deallocations of data in memory, +# thus allowing to reclaim back memory. +# +# Fragmentation is a natural process that happens with every allocator (but +# less so with Jemalloc, fortunately) and certain workloads. Normally a server +# restart is needed in order to lower the fragmentation, or at least to flush +# away all the data and create it again. However thanks to this feature +# implemented by Oran Agra for Redis 4.0 this process can happen at runtime +# in an "hot" way, while the server is running. +# +# Basically when the fragmentation is over a certain level (see the +# configuration options below) Redis will start to create new copies of the +# values in contiguous memory regions by exploiting certain specific Jemalloc +# features (in order to understand if an allocation is causing fragmentation +# and to allocate it in a better place), and at the same time, will release the +# old copies of the data. This process, repeated incrementally for all the keys +# will cause the fragmentation to drop back to normal values. +# +# Important things to understand: +# +# 1. This feature is disabled by default, and only works if you compiled Redis +# to use the copy of Jemalloc we ship with the source code of Redis. +# This is the default with Linux builds. +# +# 2. You never need to enable this feature if you don't have fragmentation +# issues. +# +# 3. Once you experience fragmentation, you can enable this feature when +# needed with the command "CONFIG SET activedefrag yes". +# +# The configuration parameters are able to fine tune the behavior of the +# defragmentation process. If you are not sure about what they mean it is +# a good idea to leave the defaults untouched. + +# Enabled active defragmentation +# activedefrag yes + +# Minimum amount of fragmentation waste to start active defrag +# active-defrag-ignore-bytes 100mb + +# Minimum percentage of fragmentation to start active defrag +# active-defrag-threshold-lower 10 + +# Maximum percentage of fragmentation at which we use maximum effort +# active-defrag-threshold-upper 100 + +# Minimal effort for defrag in CPU percentage +# active-defrag-cycle-min 25 + +# Maximal effort for defrag in CPU percentage +# active-defrag-cycle-max 75 + diff --git a/OrangeFormsOpen-MybatisPlus/.DS_Store b/OrangeFormsOpen-MybatisPlus/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fccbcd6b2ed068c4f5387797958a97189a12076a GIT binary patch literal 6148 zcmeHK!AiqG5S`Vw6nn4-4@Cv{>Y+mM&}%I5F7yZ5G)0A`32G5M#V_c=fAZ|fzwqo) z-|Q}>n>J_>L}VuHzRm1R^5#KyvqYqNv$#XlB%(SRV`T^38sm9xE4Jl4oUF_6;(Ik!F(*+QkJ9bBi{elskMV_h#izD9XGY z&(|iLRA5nhQ9u;%71*}7P2T^{%g_IQlH7>`qQJjWKvlwi*h5KfZ(S*l_galMKx5;$ m+@eWA=eA=r;H|iYW(;$h2f)B#ZV?`s{0JBsq!R^xRe?_`E1CNM literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-MybatisPlus/.gitignore b/OrangeFormsOpen-MybatisPlus/.gitignore new file mode 100644 index 00000000..e3fa94cd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/.gitignore @@ -0,0 +1,26 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +/.mvn/* + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/README.md b/OrangeFormsOpen-MybatisPlus/README.md new file mode 100644 index 00000000..980a205f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/README.md @@ -0,0 +1,21 @@ +### 服务接口文档 +--- +- Knife4j + - 服务启动后,Knife4j的文档入口地址 [http://localhost:8082/doc.html#/plus](http://localhost:8082/doc.html#/plus) +- Postman + - 无需启动服务,即可将当前工程的接口导出成Postman格式。在工程的common/common-tools/模块下,找到ExportApiApp文件,并执行main函数。 + +### 服务启动环境依赖 +--- + +执行docker-compose up -d 命令启动下面依赖的服务。 +执行docker-compose down 命令停止下面服务。 + +- Redis + - 版本:4 + - 端口: 6379 + - 推荐客户端工具 [AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesktopManager) +- Minio + - 版本:8.4.5 + - 控制台URL:需要配置Nginx,将请求导入到我们缺省设置的19000端口,之后可通过浏览器操作minio。 + - 缺省用户名密码:admin/admin123456 diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/pom.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/pom.xml new file mode 100644 index 00000000..a78c5df9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/pom.xml @@ -0,0 +1,91 @@ + + + + com.orangeforms + OrangeFormsOpen + 1.0.0 + + 4.0.0 + + application-webadmin + 1.0.0 + application-webadmin + jar + + + + + com.orangeforms + common-satoken + 1.0.0 + + + com.orangeforms + common-ext + 1.0.0 + + + com.orangeforms + common-redis + 1.0.0 + + + com.orangeforms + common-online + 1.0.0 + + + com.orangeforms + common-flow-online + 1.0.0 + + + com.orangeforms + common-log + 1.0.0 + + + com.orangeforms + common-minio + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-datafilter + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + com.orangeforms + common-dict + 1.0.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java new file mode 100644 index 00000000..86a9458a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/WebAdminApplication.java @@ -0,0 +1,23 @@ +package com.orangeforms.webadmin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + * 应用服务启动类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableAsync +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@ComponentScan("com.orangeforms") +public class WebAdminApplication { + + public static void main(String[] args) { + SpringApplication.run(WebAdminApplication.class, args); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java new file mode 100644 index 00000000..d5198b82 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/app/util/FlowIdentityExtHelper.java @@ -0,0 +1,244 @@ +package com.orangeforms.webadmin.app.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.webadmin.upms.model.SysDept; +import com.orangeforms.webadmin.upms.model.SysUser; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.webadmin.upms.model.SysDeptPost; +import com.orangeforms.webadmin.upms.service.SysDeptService; +import com.orangeforms.webadmin.upms.service.SysUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 为流程提供所需的用户身份相关的等扩展信息的帮助类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class FlowIdentityExtHelper implements BaseFlowIdentityExtHelper { + + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysUserService sysUserService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + + @PostConstruct + public void doRegister() { + flowCustomExtFactory.registerFlowIdentityExtHelper(this); + } + + @Override + public Long getLeaderDeptPostId(Long deptId) { + List deptPostIdList = sysDeptService.getLeaderDeptPostIdList(deptId); + return CollUtil.isEmpty(deptPostIdList) ? null : deptPostIdList.get(0); + } + + @Override + public Long getUpLeaderDeptPostId(Long deptId) { + List deptPostIdList = sysDeptService.getUpLeaderDeptPostIdList(deptId); + return CollUtil.isEmpty(deptPostIdList) ? null : deptPostIdList.get(0); + } + + @Override + public Map getDeptPostIdMap(Long deptId, Set postIdSet) { + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + List deptPostList = sysDeptService.getSysDeptPostList(deptId, postIdSet2); + if (CollUtil.isEmpty(deptPostList)) { + return null; + } + Map resultMap = new HashMap<>(deptPostList.size()); + deptPostList.forEach(sysDeptPost -> + resultMap.put(sysDeptPost.getPostId().toString(), sysDeptPost.getDeptPostId().toString())); + return resultMap; + } + + @Override + public Map getSiblingDeptPostIdMap(Long deptId, Set postIdSet) { + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + List deptPostList = sysDeptService.getSiblingSysDeptPostList(deptId, postIdSet2); + if (CollUtil.isEmpty(deptPostList)) { + return null; + } + Map resultMap = new HashMap<>(deptPostList.size()); + for (SysDeptPost deptPost : deptPostList) { + String deptPostId = resultMap.get(deptPost.getPostId().toString()); + if (deptPostId != null) { + deptPostId = deptPostId + "," + deptPost.getDeptPostId(); + } else { + deptPostId = deptPost.getDeptPostId().toString(); + } + resultMap.put(deptPost.getPostId().toString(), deptPostId); + } + return resultMap; + } + + @Override + public Map getUpDeptPostIdMap(Long deptId, Set postIdSet) { + SysDept sysDept = sysDeptService.getById(deptId); + if (sysDept == null || sysDept.getParentId() == null) { + return null; + } + return getDeptPostIdMap(sysDept.getParentId(), postIdSet); + } + + @Override + public Set getUsernameListByRoleIds(Set roleIdSet) { + Set usernameSet = new HashSet<>(); + Set roleIdSet2 = roleIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long roleId : roleIdSet2) { + List userList = sysUserService.getSysUserListByRoleId(roleId, filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByRoleIds(Set roleIdSet) { + List resultList = new LinkedList<>(); + Set roleIdSet2 = roleIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long roleId : roleIdSet2) { + List userList = sysUserService.getSysUserListByRoleId(roleId, filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public Set getUsernameListByDeptIds(Set deptIdSet) { + Set usernameSet = new HashSet<>(); + Set deptIdSet2 = deptIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + for (Long deptId : deptIdSet2) { + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + List userList = sysUserService.getSysUserList(filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByDeptIds(Set deptIdSet) { + List resultList = new LinkedList<>(); + Set deptIdSet2 = deptIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + for (Long deptId : deptIdSet2) { + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + List userList = sysUserService.getSysUserList(filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public Set getUsernameListByPostIds(Set postIdSet) { + Set usernameSet = new HashSet<>(); + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long postId : postIdSet2) { + List userList = sysUserService.getSysUserListByPostId(postId, filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByPostIds(Set postIdSet) { + List resultList = new LinkedList<>(); + Set postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long postId : postIdSet2) { + List userList = sysUserService.getSysUserListByPostId(postId, filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public Set getUsernameListByDeptPostIds(Set deptPostIdSet) { + Set usernameSet = new HashSet<>(); + Set deptPostIdSet2 = deptPostIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long deptPostId : deptPostIdSet2) { + List userList = sysUserService.getSysUserListByDeptPostId(deptPostId, filter, null); + this.extractAndAppendUsernameList(usernameSet, userList); + } + return usernameSet; + } + + @Override + public List getUserInfoListByDeptPostIds(Set deptPostIdSet) { + List resultList = new LinkedList<>(); + Set deptPostIdSet2 = deptPostIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + SysUser filter = new SysUser(); + filter.setUserStatus(SysUserStatus.STATUS_NORMAL); + for (Long deptPostId : deptPostIdSet2) { + List userList = sysUserService.getSysUserListByDeptPostId(deptPostId, filter, null); + if (CollUtil.isNotEmpty(userList)) { + resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class)); + } + } + return resultList; + } + + @Override + public List getUserInfoListByUsernameSet(Set usernameSet) { + List resultList = null; + List userList = sysUserService.getInList("loginName", usernameSet); + if (CollUtil.isNotEmpty(userList)) { + resultList = BeanUtil.copyToList(userList, FlowUserInfoVo.class); + } + return resultList; + } + + @Override + public Boolean supprtDataPerm() { + return true; + } + + @Override + public Map mapUserShowNameByLoginName(Set loginNameSet) { + if (CollUtil.isEmpty(loginNameSet)) { + return new HashMap<>(1); + } + Map resultMap = new HashMap<>(loginNameSet.size()); + List userList = sysUserService.getInList("loginName", loginNameSet); + userList.forEach(user -> resultMap.put(user.getLoginName(), user.getShowName())); + return resultMap; + } + + private void extractAndAppendUsernameList(Set resultUsernameList, List userList) { + List usernameList = userList.stream().map(SysUser::getLoginName).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(usernameList)) { + resultUsernameList.addAll(usernameList); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java new file mode 100644 index 00000000..dd028f9d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ApplicationConfig.java @@ -0,0 +1,38 @@ +package com.orangeforms.webadmin.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 应用程序自定义的程序属性配置文件。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "application") +public class ApplicationConfig { + /** + * 用户密码被重置之后的缺省密码 + */ + private String defaultUserPassword; + /** + * 上传文件的基础目录 + */ + private String uploadFileBaseDir; + /** + * 授信ip列表,没有填写表示全部信任。多个ip之间逗号分隔,如: http://10.10.10.1:8080,http://10.10.10.2:8080 + */ + private String credentialIpList; + /** + * Session的用户权限在Redis中的过期时间(秒)。一定要和sa-token.timeout + * 缺省值是 one day + */ + private int sessionExpiredSeconds = 86400; + /** + * 是否排他登录。 + */ + private Boolean excludeLogin = false; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java new file mode 100644 index 00000000..4820bda3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/DataSourceType.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.config; + +import com.orangeforms.common.core.constant.ApplicationConstant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表示数据源类型的常量对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DataSourceType { + + public static final int MAIN = 0; + /** + * 以下所有数据源的类都型是固定值。如果有冲突,请修改上面定义的业务服务的数据源类型值。 + */ + public static final int OPERATION_LOG = ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE; + public static final int GLOBAL_DICT = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE; + public static final int COMMON_FLOW_AND_ONLINE = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE; + + private static final Map TYPE_MAP = new HashMap<>(8); + static { + TYPE_MAP.put("main", MAIN); + TYPE_MAP.put("operation-log", OPERATION_LOG); + TYPE_MAP.put("global-dict", GLOBAL_DICT); + TYPE_MAP.put("common-flow-online", COMMON_FLOW_AND_ONLINE); + } + + /** + * 根据名称获取字典类型。 + * + * @param name 数据源在配置中的名称。 + * @return 返回可用于多数据源切换的数据源类型。 + */ + public static Integer getDataSourceTypeByName(String name) { + return TYPE_MAP.get(name); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataSourceType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java new file mode 100644 index 00000000..350602db --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/FilterConfig.java @@ -0,0 +1,60 @@ +package com.orangeforms.webadmin.config; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import jakarta.servlet.Filter; +import java.nio.charset.StandardCharsets; + +/** + * 这里主要配置Web的各种过滤器和监听器等Servlet容器组件。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class FilterConfig { + + /** + * 配置Ajax跨域过滤器。 + */ + @Bean + public CorsFilter corsFilterRegistration(ApplicationConfig applicationConfig) { + UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + if (StringUtils.isNotBlank(applicationConfig.getCredentialIpList())) { + if ("*".equals(applicationConfig.getCredentialIpList())) { + corsConfiguration.addAllowedOriginPattern("*"); + } else { + String[] credentialIpList = StringUtils.split(applicationConfig.getCredentialIpList(), ","); + if (credentialIpList.length > 0) { + for (String ip : credentialIpList) { + corsConfiguration.addAllowedOrigin(ip); + } + } + } + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + corsConfiguration.setAllowCredentials(true); + configSource.registerCorsConfiguration("/**", corsConfiguration); + } + return new CorsFilter(configSource); + } + + @Bean + public FilterRegistrationBean characterEncodingFilterRegistration() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new org.springframework.web.filter.CharacterEncodingFilter()); + filterRegistrationBean.addUrlPatterns("/*"); + filterRegistrationBean.addInitParameter("encoding", StandardCharsets.UTF_8.name()); + // forceEncoding强制response也被编码,另外即使request中已经设置encoding,forceEncoding也会重新设置 + filterRegistrationBean.addInitParameter("forceEncoding", "true"); + filterRegistrationBean.setAsyncSupported(true); + return filterRegistrationBean; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java new file mode 100644 index 00000000..1d75ac6d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/InterceptorConfig.java @@ -0,0 +1,21 @@ +package com.orangeforms.webadmin.config; + +import com.orangeforms.webadmin.interceptor.AuthenticationInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 所有的项目拦截器都在这里集中配置 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/admin/**"); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java new file mode 100644 index 00000000..bb09bf79 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/MultiDataSourceConfig.java @@ -0,0 +1,77 @@ +package com.orangeforms.webadmin.config; + +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.orangeforms.common.core.config.BaseMultiDataSourceConfig; +import com.orangeforms.common.core.config.DynamicDataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.mybatis.spring.annotation.MapperScan; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/** + * 多数据源配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@EnableTransactionManagement +@MapperScan(value = {"com.orangeforms.webadmin.*.dao", "com.orangeforms.common.*.dao"}) +public class MultiDataSourceConfig extends BaseMultiDataSourceConfig { + + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.main") + public DataSource mainDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + /** + * 默认生成的用于保存操作日志的数据源,可根据需求修改。 + * 这里我们还是非常推荐给操作日志使用独立的数据源,这样便于今后的数据迁移。 + */ + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.operation-log") + public DataSource operationLogDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + /** + * 默认生成的用于全局编码字典的数据源,可根据需求修改。 + */ + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.global-dict") + public DataSource globalDictDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + /** + * 默认生成的用于在线表单内部表的数据源,可根据需求修改。 + * 这里我们还是非常推荐使用独立数据源,这样便于今后的服务拆分。 + */ + @Bean(initMethod = "init", destroyMethod = "close") + @ConfigurationProperties(prefix = "spring.datasource.druid.common-flow-online") + public DataSource commonFlowAndOnlineDataSource() { + return super.applyCommonProps(DruidDataSourceBuilder.create().build()); + } + + @Bean + @Primary + public DynamicDataSource dataSource() { + Map targetDataSources = new HashMap<>(1); + targetDataSources.put(DataSourceType.MAIN, mainDataSource()); + targetDataSources.put(DataSourceType.OPERATION_LOG, operationLogDataSource()); + targetDataSources.put(DataSourceType.GLOBAL_DICT, globalDictDataSource()); + targetDataSources.put(DataSourceType.COMMON_FLOW_AND_ONLINE, commonFlowAndOnlineDataSource()); + // 如果当前工程支持在线表单,这里请务必保证upms数据表所在数据库为缺省数据源。 + DynamicDataSource dynamicDataSource = new DynamicDataSource(); + dynamicDataSource.setTargetDataSources(targetDataSources); + dynamicDataSource.setDefaultTargetDataSource(mainDataSource()); + return dynamicDataSource; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java new file mode 100644 index 00000000..e827057a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/config/ThirdPartyAuthConfig.java @@ -0,0 +1,66 @@ +package com.orangeforms.webadmin.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.Data; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 第三方应用鉴权配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "third-party") +public class ThirdPartyAuthConfig implements InitializingBean { + + private List auth; + + private Map applicationMap; + + @Override + public void afterPropertiesSet() throws Exception { + if (CollUtil.isEmpty(auth)) { + applicationMap = new HashMap<>(1); + } else { + applicationMap = auth.stream().collect(Collectors.toMap(AuthProperties::getAppCode, c -> c)); + } + } + + @Data + public static class AuthProperties { + /** + * 应用Id。 + */ + private String appCode; + /** + * 身份验证相关url的base地址。 + */ + private String baseUrl; + /** + * 是否为橙单框架。 + */ + private Boolean orangeFramework = true; + /** + * token的Http Request Header的key + */ + private String tokenHeaderKey; + /** + * 数据权限和用户操作权限缓存过期时间,单位秒。 + */ + private Integer permExpiredSeconds = 86400; + /** + * 用户Token缓存过期时间,单位秒。 + * 如果为0,则每次都要去第三方服务进行验证。 + */ + private Integer tokenExpiredSeconds = 0; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java new file mode 100644 index 00000000..f2329ff6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/interceptor/AuthenticationInterceptor.java @@ -0,0 +1,281 @@ +package com.orangeforms.webadmin.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.DataPermRuleType; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.satoken.util.SaTokenUtil; +import com.orangeforms.webadmin.config.ThirdPartyAuthConfig; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 登录用户Token验证、生成和权限验证的拦截器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class AuthenticationInterceptor implements HandlerInterceptor { + + private final ThirdPartyAuthConfig thirdPartyAuthConfig = + ApplicationContextHolder.getBean("thirdPartyAuthConfig"); + + private final RedissonClient redissonClient = ApplicationContextHolder.getBean(RedissonClient.class); + private final CacheManager cacheManager = ApplicationContextHolder.getBean("caffeineCacheManager"); + + private final SaTokenUtil saTokenUtil = + ApplicationContextHolder.getBean("saTokenUtil"); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String appCode = this.getAppCodeFromRequest(request); + if (StrUtil.isNotBlank(appCode)) { + return this.handleThirdPartyRequest(appCode, request); + } + ResponseResult result = saTokenUtil.handleAuthIntercept(request, handler); + if (!result.isSuccess()) { + ResponseResult.output(result.getHttpStatus(), result); + return false; + } + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + // 这里需要空注解,否则sonar会不happy。 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + // 这里需要空注解,否则sonar会不happy。 + } + + private String getTokenFromRequest(HttpServletRequest request, String appCode) { + ThirdPartyAuthConfig.AuthProperties prop = thirdPartyAuthConfig.getApplicationMap().get(appCode); + String token = request.getHeader(prop.getTokenHeaderKey()); + if (StrUtil.isBlank(token)) { + token = request.getParameter(prop.getTokenHeaderKey()); + } + if (StrUtil.isBlank(token)) { + token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN); + } + return token; + } + + private String getAppCodeFromRequest(HttpServletRequest request) { + String appCode = request.getHeader("AppCode"); + if (StrUtil.isBlank(appCode)) { + appCode = request.getParameter("AppCode"); + } + return appCode; + } + + private boolean handleThirdPartyRequest(String appCode, HttpServletRequest request) throws IOException { + String token = this.getTokenFromRequest(request, appCode); + ThirdPartyAuthConfig.AuthProperties authProps = thirdPartyAuthConfig.getApplicationMap().get(appCode); + if (authProps == null) { + String msg = StrFormatter.format("请求的 appCode[{}] 信息,在当前服务中尚未配置!", appCode); + ResponseResult.output(ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, msg)); + return false; + } + ResponseResult result = this.getAndCacheThirdPartyTokenData(authProps, token); + if (!result.isSuccess()) { + ResponseResult.output(result.getHttpStatus(), + ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, result.getErrorMessage())); + return false; + } + TokenData tokenData = result.getData(); + tokenData.setAppCode(appCode); + tokenData.setSessionId(this.prependAppCode(authProps.getAppCode(), tokenData.getSessionId())); + TokenData.addToRequest(tokenData); + String url = request.getRequestURI(); + if (Boolean.FALSE.equals(tokenData.getIsAdmin()) + && !this.hasThirdPartyPermission(authProps, tokenData, url)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return false; + } + return true; + } + + private ResponseResult getAndCacheThirdPartyTokenData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + if (authProps.getTokenExpiredSeconds() == 0) { + return this.getThirdPartyTokenData(authProps, token); + } + String tokeKey = this.prependAppCode(authProps.getAppCode(), RedisKeyUtil.makeSessionIdKey(token)); + RBucket sessionData = redissonClient.getBucket(tokeKey); + if (sessionData.isExists()) { + return ResponseResult.success(JSON.parseObject(sessionData.get(), TokenData.class)); + } + ResponseResult responseResult = this.getThirdPartyTokenData(authProps, token); + if (responseResult.isSuccess()) { + sessionData.set(JSON.toJSONString(responseResult.getData()), authProps.getTokenExpiredSeconds(), TimeUnit.SECONDS); + } + return responseResult; + } + + private String prependAppCode(String appCode, String key) { + return appCode.toUpperCase() + ":" + key; + } + + private ResponseResult getThirdPartyTokenData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + try { + String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getTokenData", token); + return JSON.parseObject(resultData, new TypeReference>() {}); + } catch (MyRuntimeException ex) { + return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage()); + } + } + + private ResponseResult getThirdPartyPermData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + try { + String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getPermData", token); + return JSON.parseObject(resultData, new TypeReference>() {}); + } catch (MyRuntimeException ex) { + return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage()); + } + } + + private String invokeThirdPartyUrl(String url, String token) { + Map headerMap = new HashMap<>(1); + headerMap.put("Authorization", token); + StringBuilder fullUrl = new StringBuilder(128); + fullUrl.append(url).append("?token=").append(token); + HttpResponse httpResponse = HttpUtil.createGet(fullUrl.toString()).addHeaders(headerMap).execute(); + if (!httpResponse.isOk()) { + String msg = StrFormatter.format( + "Failed to call [{}] with ERROR HTTP Status [{}] and [{}].", + url, httpResponse.getStatus(), httpResponse.body()); + log.error(msg); + throw new MyRuntimeException(msg); + } + return httpResponse.body(); + } + + @SuppressWarnings("unchecked") + private boolean hasThirdPartyPermission( + ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, String url) { + // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。 + String permKey = RedisKeyUtil.makeSessionPermIdKey(tokenData.getSessionId()); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL"); + Cache.ValueWrapper wrapper = cache.get(permKey); + if (wrapper != null) { + Object cachedData = wrapper.get(); + if (cachedData != null) { + return ((Set) cachedData).contains(url); + } + } + Set localPermSet; + RSet permSet = redissonClient.getSet(permKey); + if (permSet.isExists()) { + localPermSet = permSet.readAll(); + cache.put(permKey, localPermSet); + return localPermSet.contains(url); + } + ResponseResult responseResult = this.getThirdPartyPermData(authProps, tokenData.getToken()); + this.cacheThirdPartyDataPermData(authProps, tokenData, responseResult.getData().getDataPerms()); + if (CollUtil.isEmpty(responseResult.getData().urlPerms)) { + return false; + } + permSet.addAll(responseResult.getData().urlPerms); + permSet.expire(authProps.getPermExpiredSeconds(), TimeUnit.SECONDS); + localPermSet = new HashSet<>(responseResult.getData().urlPerms); + cache.put(permKey, localPermSet); + return localPermSet.contains(url); + } + + private void cacheThirdPartyDataPermData( + ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, List dataPerms) { + if (CollUtil.isEmpty(dataPerms)) { + return; + } + Map> dataPermMap = + dataPerms.stream().collect(Collectors.groupingBy(ThirdPartyAppDataPermData::getRuleType)); + Map> normalizedDataPermMap = new HashMap<>(dataPermMap.size()); + for (Map.Entry> entry : dataPermMap.entrySet()) { + List ruleTypeDataPermDataList; + if (entry.getKey().equals(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) { + ruleTypeDataPermDataList = + normalizedDataPermMap.computeIfAbsent(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, k -> new LinkedList<>()); + } else { + ruleTypeDataPermDataList = + normalizedDataPermMap.computeIfAbsent(entry.getKey(), k -> new LinkedList<>()); + } + ruleTypeDataPermDataList.addAll(entry.getValue()); + } + Map resultDataPermMap = new HashMap<>(normalizedDataPermMap.size()); + for (Map.Entry> entry : normalizedDataPermMap.entrySet()) { + if (entry.getKey().equals(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST)) { + String deptIds = entry.getValue().stream() + .map(ThirdPartyAppDataPermData::getDeptIds).collect(Collectors.joining(",")); + resultDataPermMap.put(entry.getKey(), deptIds); + } else { + resultDataPermMap.put(entry.getKey(), "null"); + } + } + Map> menuDataPermMap = new HashMap<>(1); + menuDataPermMap.put(ApplicationConstant.DATA_PERM_ALL_MENU_ID, resultDataPermMap); + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + RBucket bucket = redissonClient.getBucket(dataPermSessionKey); + bucket.set(JSON.toJSONString(menuDataPermMap), authProps.getPermExpiredSeconds(), TimeUnit.SECONDS); + } + + @Data + public static class ThirdPartyAppPermData { + /** + * 当前用户会话可访问的url接口地址列表。 + */ + private List urlPerms; + /** + * 当前用户会话的数据权限列表。 + */ + private List dataPerms; + } + + @Data + public static class ThirdPartyAppDataPermData { + /** + * 数据权限的规则类型。需要按照橙单的约定返回。具体值可参考DataPermRuleType常量类。 + */ + private Integer ruleType; + /** + * 部门Id集合,多个部门Id之间逗号分隔。 + * 注意:仅当ruleType为3、4、5时需要包含该字段值。 + */ + private String deptIds; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java new file mode 100644 index 00000000..dbca8a5b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuExtraData.java @@ -0,0 +1,55 @@ +package com.orangeforms.webadmin.upms.bo; + +import lombok.Data; + +import java.util.List; + +/** + * 菜单扩展数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SysMenuExtraData { + + /** + * 路由名称。 + */ + private String formRouterName; + + /** + * 在线表单。 + */ + private Long onlineFormId; + + /** + * 报表页面。 + */ + private Long reportPageId; + + /** + * 流程。 + */ + private Long onlineFlowEntryId; + + /** + * 目标url。 + */ + private String targetUrl; + + /** + * 绑定类型。 + */ + private Integer bindType; + + /** + * 前端使用的菜单编码。仅当选择satoken权限框架时使用。 + */ + private String menuCode; + + /** + * 菜单关联的后台使用的权限字列表。仅当选择satoken权限框架时使用。 + */ + private List permCodeList; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java new file mode 100644 index 00000000..8c429d37 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/bo/SysMenuPerm.java @@ -0,0 +1,66 @@ +package com.orangeforms.webadmin.upms.bo; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 菜单相关的业务对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SysMenuPerm { + + /** + * 菜单Id。 + */ + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null + */ + private Long parentId; + + /** + * 菜单显示名称。 + */ + private String menuName; + + /** + * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + private Integer menuType; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + private Long onlineFormId; + + /** + * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。 + */ + private Integer onlineMenuPermType; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + private Long onlineFlowEntryId; + + /** + * 关联权限URL集合。 + */ + Set permUrlSet = new HashSet<>(); + + /** + * 关联的某一个url。 + */ + String url; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java new file mode 100644 index 00000000..df90d312 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/GlobalDictController.java @@ -0,0 +1,340 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dict.dto.GlobalDictDto; +import com.orangeforms.common.dict.dto.GlobalDictItemDto; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictItemService; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.dict.util.GlobalDictOperationHelper; +import com.orangeforms.common.dict.vo.GlobalDictVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 全局通用字典操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "全局字典管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/globalDict") +public class GlobalDictController { + + @Autowired + private GlobalDictService globalDictService; + @Autowired + private GlobalDictItemService globalDictItemService; + @Autowired + private GlobalDictOperationHelper globalDictOperationHelper; + + /** + * 新增全局字典接口。 + * + * @param globalDictDto 新增字典对象。 + * @return 保存后的字典对象。 + */ + @ApiOperationSupport(ignoreParameters = {"globalDictDto.dictId"}) + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody GlobalDictDto globalDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 这里必须手动校验字典编码是否存在,因为我们缺省的实现是逻辑删除,所以字典编码字段没有设置为唯一索引。 + if (globalDictService.existDictCode(globalDictDto.getDictCode())) { + errorMessage = "数据验证失败,字典编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDict globalDict = MyModelUtil.copyTo(globalDictDto, GlobalDict.class); + globalDictService.saveNew(globalDict); + return ResponseResult.success(globalDict.getDictId()); + } + + /** + * 更新全局字典操作。 + * + * @param globalDictDto 更新全局字典对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody GlobalDictDto globalDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDict originalGlobalDict = globalDictService.getById(globalDictDto.getDictId()); + if (originalGlobalDict == null) { + errorMessage = "数据验证失败,当前全局字典并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + GlobalDict globalDict = MyModelUtil.copyTo(globalDictDto, GlobalDict.class); + if (ObjectUtil.notEqual(globalDict.getDictCode(), originalGlobalDict.getDictCode()) + && globalDictService.existDictCode(globalDict.getDictCode())) { + errorMessage = "数据验证失败,字典编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!globalDictService.update(globalDict, originalGlobalDict)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除指定的全局字典。 + * + * @param dictId 指定全局字典主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody(required = true) Long dictId) { + if (!globalDictService.remove(dictId)) { + String errorMessage = "数据操作失败,全局字典Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 查看全局字典列表。 + * + * @param globalDictDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含角色列表。 + */ + @SaCheckPermission("globalDict.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody GlobalDictDto globalDictDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + GlobalDict filter = MyModelUtil.copyTo(globalDictDtoFilter, GlobalDict.class); + List globalDictList = + globalDictService.getGlobalDictList(filter, MyOrderParam.buildOrderBy(orderParam, GlobalDict.class)); + List globalDictVoList = + MyModelUtil.copyCollectionTo(globalDictList, GlobalDictVo.class); + long totalCount = 0L; + if (globalDictList instanceof Page) { + totalCount = ((Page) globalDictList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(globalDictVoList, totalCount)); + } + + /** + * 新增全局字典项目接口。 + * + * @param globalDictItemDto 新增字典项目对象。 + * @return 保存后的字典对象。 + */ + @SaCheckPermission("globalDict.update") + @ApiOperationSupport(ignoreParameters = {"globalDictItemDto.id"}) + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/addItem") + public ResponseResult addItem(@MyRequestBody GlobalDictItemDto globalDictItemDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictItemDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!globalDictService.existDictCode(globalDictItemDto.getDictCode())) { + errorMessage = "数据验证失败,字典编码不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (globalDictItemService.existDictCodeAndItemId( + globalDictItemDto.getDictCode(), globalDictItemDto.getItemId())) { + errorMessage = "数据验证失败,该字典编码的项目Id已存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDictItem globalDictItem = MyModelUtil.copyTo(globalDictItemDto, GlobalDictItem.class); + globalDictItemService.saveNew(globalDictItem); + return ResponseResult.success(globalDictItem.getId()); + } + + /** + * 更新全局字典项目。 + * + * @param globalDictItemDto 更新全局字典项目对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateItem") + public ResponseResult updateItem(@MyRequestBody GlobalDictItemDto globalDictItemDto) { + String errorMessage = MyCommonUtil.getModelValidationError(globalDictItemDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + GlobalDictItem originalGlobalDictItem = globalDictItemService.getById(globalDictItemDto.getId()); + if (originalGlobalDictItem == null) { + errorMessage = "数据验证失败,当前全局字典项目并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + GlobalDictItem globalDictItem = MyModelUtil.copyTo(globalDictItemDto, GlobalDictItem.class); + if (ObjectUtil.notEqual(globalDictItem.getDictCode(), originalGlobalDictItem.getDictCode())) { + errorMessage = "数据验证失败,字典项目的字典编码不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(globalDictItem.getItemId(), originalGlobalDictItem.getItemId()) + && globalDictItemService.existDictCodeAndItemId(globalDictItem.getDictCode(), globalDictItem.getItemId())) { + errorMessage = "数据验证失败,该字典编码已经包含了该项目Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!globalDictItemService.update(globalDictItem, originalGlobalDictItem)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 更新全局字典项目的状态。 + * + * @param id 更新全局字典项目主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateItemStatus") + public ResponseResult updateItemStatus( + @MyRequestBody(required = true) Long id, @MyRequestBody(required = true) Integer status) { + String errorMessage; + GlobalDictItem dictItem = globalDictItemService.getById(id); + if (dictItem == null) { + errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (ObjectUtil.notEqual(dictItem.getStatus(), status)) { + globalDictItemService.updateStatus(dictItem, status); + } + return ResponseResult.success(); + } + + /** + * 删除指定编码的全局字典项目。 + * + * @param id 主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("globalDict.update") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/deleteItem") + public ResponseResult deleteItem(@MyRequestBody(required = true) Long id) { + String errorMessage; + GlobalDictItem dictItem = globalDictItemService.getById(id); + if (dictItem == null) { + errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!globalDictItemService.remove(dictItem)) { + errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 将当前字典表的数据重新加载到缓存中。 + * 由于缓存的数据更新,在add/update/delete等接口均有同步处理。因此该接口仅当同步过程中出现问题时, + * 可手工调用,或者每天晚上定时同步一次。 + */ + @SaCheckPermission("globalDict.view") + @OperationLog(type = SysOperationLogType.RELOAD_CACHE) + @GetMapping("/reloadCachedData") + public ResponseResult reloadCachedData(@RequestParam String dictCode) { + globalDictService.reloadCachedData(dictCode); + return ResponseResult.success(true); + } + + /** + * 获取指定字典编码的全局字典项目。字典的键值为[itemId, itemName]。 + * NOTE: 白名单接口。 + * + * @param dictCode 字典编码。 + * @param itemIdType 字典项目的ItemId值转换到的目标类型。可能值为Integer或Long。 + * @return 应答结果对象。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict( + @RequestParam String dictCode, @RequestParam(required = false) String itemIdType) { + List resultList = + globalDictService.getGlobalDictItemListFromCache(dictCode, null); + resultList = resultList.stream() + .sorted(Comparator.comparing(GlobalDictItem::getStatus)) + .sorted(Comparator.comparing(GlobalDictItem::getShowOrder)) + .collect(Collectors.toList()); + return ResponseResult.success(globalDictOperationHelper.toDictDataList(resultList, itemIdType)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * NOTE: 白名单接口。 + * + * @param dictCode 字典编码。 + * @param itemIds 字典项目Id集合。 + * @param itemIdType 字典项目的ItemId值转换到的目标类型。可能值为Integer或Long。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds( + @RequestParam String dictCode, + @RequestParam List itemIds, + @RequestParam(required = false) String itemIdType) { + List resultList = + globalDictService.getGlobalDictItemListFromCache(dictCode, new HashSet<>(itemIds)); + return ResponseResult.success(globalDictOperationHelper.toDictDataList(resultList, itemIdType)); + } + + /** + * 白名单接口,登录用户均可访问。以字典形式返回全部字典数据集合。 + * fullResultList中的字典列表全部取自于数据库,而cachedResultList全部取自于缓存,前端负责比对。 + * + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listAll") + public ResponseResult listAll(@RequestParam String dictCode) { + List fullResultList = + globalDictItemService.getGlobalDictItemListByDictCode(dictCode); + List cachedList = + globalDictService.getGlobalDictItemListFromCache(dictCode, null); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("fullResultList", globalDictOperationHelper.toDictDataList2(fullResultList)); + jsonObject.put("cachedResultList", globalDictOperationHelper.toDictDataList2(cachedList)); + return ResponseResult.success(jsonObject); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java new file mode 100644 index 00000000..656c9a38 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginController.java @@ -0,0 +1,475 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONArray; +import com.github.xiaoymin.knife4j.annotations.ApiSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.config.ApplicationConfig; +import com.orangeforms.webadmin.upms.bo.SysMenuExtraData; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.webadmin.upms.model.constant.SysUserType; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import com.orangeforms.webadmin.upms.model.constant.SysOnlineMenuPermType; +import com.orangeforms.common.flow.online.service.FlowOnlineOperationService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.upload.*; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.satoken.util.SaTokenUtil; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 登录接口控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@ApiSupport(order = 1) +@Tag(name = "用户登录接口") +@DisableDataFilter +@Slf4j +@RestController +@RequestMapping("/admin/upms/login") +public class LoginController { + + @Autowired + private SysUserService sysUserService; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysMenuService sysMenuService; + @Autowired + private SysPostService sysPostService; + @Autowired + private SysRoleService sysRoleService; + @Autowired + private SysDataPermService sysDataPermService; + @Autowired + private SysPermWhitelistService sysPermWhitelistService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private FlowOnlineOperationService flowOnlineOperationService; + @Autowired + private ApplicationConfig appConfig; + @Autowired + private RedissonClient redissonClient; + @Autowired + private SessionCacheHelper cacheHelper; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private SaTokenUtil saTokenUtil; + + private static final String IS_ADMIN = "isAdmin"; + private static final String SHOW_NAME_FIELD = "showName"; + private static final String SHOW_ORDER_FIELD = "showOrder"; + private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl"; + + /** + * 登录接口。 + * + * @param loginName 登录名。 + * @param password 密码。 + * @return 应答结果对象,其中包括Token数据,以及菜单列表。 + */ + @Parameter(name = "loginName", example = "admin") + @Parameter(name = "password", example = "IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D") + @SaIgnore + @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false) + @PostMapping("/doLogin") + public ResponseResult doLogin( + @MyRequestBody String loginName, + @MyRequestBody String password) throws UnsupportedEncodingException { + if (MyCommonUtil.existBlankArgument(loginName, password)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.verifyAndHandleLoginUser(loginName, password); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + JSONObject jsonData = this.buildLoginDataAndLogin(verifyResult.getData()); + return ResponseResult.success(jsonData); + } + + /** + * 登出操作。同时将Session相关的信息从缓存中删除。 + * + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.LOGOUT) + @PostMapping("/doLogout") + public ResponseResult doLogout() { + String sessionId = TokenData.takeFromRequest().getSessionId(); + redissonClient.getBucket(TokenData.takeFromRequest().getMySessionId()).deleteAsync(); + redissonClient.getBucket(RedisKeyUtil.makeSessionPermCodeKey(sessionId)).deleteAsync(); + redissonClient.getBucket(RedisKeyUtil.makeSessionPermIdKey(sessionId)).deleteAsync(); + sysDataPermService.removeDataPermCache(sessionId); + cacheHelper.removeAllSessionCache(sessionId); + StpUtil.logout(); + return ResponseResult.success(); + } + + /** + * 在登录之后,通过token再次获取登录信息。 + * 用于在当前浏览器登录系统后,在新tab页中可以免密登录。 + * + * @return 应答结果对象,其中包括JWT的Token数据,以及菜单列表。 + */ + @GetMapping("/getLoginInfo") + public ResponseResult getLoginInfo() { + TokenData tokenData = TokenData.takeFromRequest(); + JSONObject jsonData = new JSONObject(); + jsonData.put(SHOW_NAME_FIELD, tokenData.getShowName()); + jsonData.put(IS_ADMIN, tokenData.getIsAdmin()); + if (StrUtil.isNotBlank(tokenData.getHeadImageUrl())) { + jsonData.put(HEAD_IMAGE_URL_FIELD, tokenData.getHeadImageUrl()); + } + Collection allMenuList; + if (BooleanUtil.isTrue(tokenData.getIsAdmin())) { + allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD); + } else { + allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds()); + } + List menuCodeList = new LinkedList<>(); + OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet); + OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet); + allMenuList.stream().filter(m -> m.getExtraData() != null) + .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class))); + this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList); + return ResponseResult.success(jsonData); + } + + /** + * 返回所有可用的权限字列表。 + * + * @return 整个系统所有可用的权限字列表。 + */ + @GetMapping("/getAllPermCodes") + public ResponseResult> getAllPermCodes() { + List permCodes = saTokenUtil.getAllPermCodes(); + return ResponseResult.success(permCodes); + } + + /** + * 用户修改自己的密码。 + * + * @param oldPass 原有密码。 + * @param newPass 新密码。 + * @return 应答结果对象。 + */ + @PostMapping("/changePassword") + public ResponseResult changePassword( + @MyRequestBody String oldPass, @MyRequestBody String newPass) throws UnsupportedEncodingException { + if (MyCommonUtil.existBlankArgument(newPass, oldPass)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + SysUser user = sysUserService.getById(tokenData.getUserId()); + oldPass = URLDecoder.decode(oldPass, StandardCharsets.UTF_8.name()); + // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。 + // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。 + oldPass = RsaUtil.decrypt(oldPass, ApplicationConstant.PRIVATE_KEY); + if (user == null || !passwordEncoder.matches(oldPass, user.getPassword())) { + return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD); + } + newPass = URLDecoder.decode(newPass, StandardCharsets.UTF_8.name()); + newPass = RsaUtil.decrypt(newPass, ApplicationConstant.PRIVATE_KEY); + if (!sysUserService.changePassword(tokenData.getUserId(), newPass)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 上传并修改用户头像。 + * + * @param uploadFile 上传的头像文件。 + */ + @PostMapping("/changeHeadImage") + public void changeHeadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD); + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, true, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + responseInfo.setDownloadUri("/admin/upms/login/downloadHeadImage"); + String newHeadImage = JSONArray.toJSONString(CollUtil.newArrayList(responseInfo)); + if (!sysUserService.changeHeadImage(TokenData.takeFromRequest().getUserId(), newHeadImage)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST)); + return; + } + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + /** + * 下载用户头像。 + * + * @param filename 文件名。如果没有提供该参数,就从当前记录的指定字段中读取。 + * @param response Http 应答对象。 + */ + @GetMapping("/downloadHeadImage") + public void downloadHeadImage(String filename, HttpServletResponse response) { + try { + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD); + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + upDownloader.doDownload(appConfig.getUploadFileBaseDir(), + SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, filename, true, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + private ResponseResult verifyAndHandleLoginUser( + String loginName, String password) throws UnsupportedEncodingException { + String errorMessage; + SysUser user = sysUserService.getSysUserByLoginName(loginName); + password = URLDecoder.decode(password, StandardCharsets.UTF_8.name()); + // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。 + // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。 + password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY); + if (user == null || !passwordEncoder.matches(password, user.getPassword())) { + return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD); + } + if (user.getUserStatus() == SysUserStatus.STATUS_LOCKED) { + errorMessage = "登录失败,用户账号被锁定!"; + return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage); + } + if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) { + String deviceType = MyCommonUtil.getDeviceTypeWithString(); + LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class); + String loginId = SaTokenUtil.makeLoginId(userInfo); + StpUtil.kickout(loginId, deviceType); + } + return ResponseResult.success(user); + } + + private JSONObject buildLoginDataAndLogin(SysUser user) { + TokenData tokenData = this.loginAndCreateToken(user); + // 这里手动将TokenData存入request,便于OperationLogAspect统一处理操作日志。 + TokenData.addToRequest(tokenData); + JSONObject jsonData = this.createResponseData(user); + Collection allMenuList; + boolean isAdmin = user.getUserType() == SysUserType.TYPE_ADMIN; + if (isAdmin) { + allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD); + } else { + allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds()); + } + allMenuList.stream().filter(m -> m.getExtraData() != null) + .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class))); + Collection permCodeList = new LinkedList<>(); + allMenuList.stream().filter(m -> m.getExtraObject() != null) + .forEach(m -> CollUtil.addAll(permCodeList, m.getExtraObject().getPermCodeList())); + Set permSet = new HashSet<>(); + if (!isAdmin) { + // 所有登录用户都有白名单接口的访问权限。 + CollUtil.addAll(permSet, sysPermWhitelistService.getWhitelistPermList()); + } + List menuCodeList = new LinkedList<>(); + OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet); + OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList); + CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet); + if (!isAdmin) { + permSet.addAll(onlinePermData.permUrlSet); + permSet.addAll(onlineFlowPermData.permUrlSet); + String sessionId = tokenData.getSessionId(); + // 缓存用户的权限资源,这里缓存的是基于URL验证的权限资源,比如在线表单、工作流和数据表中的白名单资源。 + this.putUserSysPermCache(sessionId, permSet); + // 缓存权限字字段,StpInterfaceImpl中会从缓存中读取,并交给satoken进行接口权限的验证。 + this.putUserSysPermCodeCache(sessionId, permCodeList); + sysDataPermService.putDataPermCache(sessionId, user.getUserId(), user.getDeptId()); + } + this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList); + return jsonData; + } + + private TokenData loginAndCreateToken(SysUser user) { + String deviceType = MyCommonUtil.getDeviceTypeWithString(); + LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class); + String loginId = SaTokenUtil.makeLoginId(userInfo); + StpUtil.login(loginId, deviceType); + SaSession session = StpUtil.getTokenSession(); + TokenData tokenData = this.buildTokenData(user, session.getId(), StpUtil.getLoginDevice()); + String mySessionId = RedisKeyUtil.getSessionIdPrefix(tokenData, user.getLoginName()) + MyCommonUtil.generateUuid(); + tokenData.setMySessionId(mySessionId); + tokenData.setToken(session.getToken()); + redissonClient.getBucket(mySessionId) + .set(JSON.toJSONString(tokenData), appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + session.set(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData); + return tokenData; + } + + private JSONObject createResponseData(SysUser user) { + JSONObject jsonData = new JSONObject(); + jsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, StpUtil.getTokenValue()); + jsonData.put(SHOW_NAME_FIELD, user.getShowName()); + jsonData.put(IS_ADMIN, user.getUserType() == SysUserType.TYPE_ADMIN); + if (user.getDeptId() != null) { + SysDept dept = sysDeptService.getById(user.getDeptId()); + jsonData.put("deptName", dept.getDeptName()); + } + if (StrUtil.isNotBlank(user.getHeadImageUrl())) { + jsonData.put(HEAD_IMAGE_URL_FIELD, user.getHeadImageUrl()); + } + return jsonData; + } + + private void appendResponseMenuAndPermCodeData( + JSONObject responseData, Collection allMenuList, Collection menuCodeList) { + allMenuList.stream() + .filter(m -> m.getExtraObject() != null && StrUtil.isNotBlank(m.getExtraObject().getMenuCode())) + .forEach(m -> CollUtil.addAll(menuCodeList, m.getExtraObject().getMenuCode())); + List menuList = allMenuList.stream() + .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList()); + responseData.put("menuList", menuList); + responseData.put("permCodeList", menuCodeList); + } + + private TokenData buildTokenData(SysUser user, String sessionId, String deviceType) { + TokenData tokenData = new TokenData(); + tokenData.setSessionId(sessionId); + tokenData.setUserId(user.getUserId()); + tokenData.setDeptId(user.getDeptId()); + tokenData.setLoginName(user.getLoginName()); + tokenData.setShowName(user.getShowName()); + tokenData.setIsAdmin(user.getUserType().equals(SysUserType.TYPE_ADMIN)); + tokenData.setLoginIp(IpUtil.getRemoteIpAddress(ContextUtil.getHttpRequest())); + tokenData.setLoginTime(new Date()); + tokenData.setDeviceType(deviceType); + tokenData.setHeadImageUrl(user.getHeadImageUrl()); + List userPostList = sysPostService.getSysUserPostListByUserId(user.getUserId()); + if (CollUtil.isNotEmpty(userPostList)) { + Set deptPostIdSet = userPostList.stream().map(SysUserPost::getDeptPostId).collect(Collectors.toSet()); + tokenData.setDeptPostIds(StrUtil.join(",", deptPostIdSet)); + Set postIdSet = userPostList.stream().map(SysUserPost::getPostId).collect(Collectors.toSet()); + tokenData.setPostIds(StrUtil.join(",", postIdSet)); + } + List userRoleList = sysRoleService.getSysUserRoleListByUserId(user.getUserId()); + if (CollUtil.isNotEmpty(userRoleList)) { + Set userRoleIdSet = userRoleList.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet()); + tokenData.setRoleIds(StrUtil.join(",", userRoleIdSet)); + } + return tokenData; + } + + private void putUserSysPermCache(String sessionId, Collection permUrlSet) { + if (CollUtil.isEmpty(permUrlSet)) { + return; + } + String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId); + RSet redisPermSet = redissonClient.getSet(sessionPermKey); + redisPermSet.addAll(permUrlSet); + redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + } + + private void putUserSysPermCodeCache(String sessionId, Collection permCodeSet) { + if (CollUtil.isEmpty(permCodeSet)) { + return; + } + String sessionPermCodeKey = RedisKeyUtil.makeSessionPermCodeKey(sessionId); + RSet redisPermSet = redissonClient.getSet(sessionPermCodeKey); + redisPermSet.addAll(permCodeSet); + redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + } + + private OnlinePermData getOnlineMenuPermData(Collection allMenuList) { + List onlineMenuList = allMenuList.stream() + .filter(m -> m.getOnlineFormId() != null && m.getMenuType().equals(SysMenuType.TYPE_BUTTON)) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(onlineMenuList)) { + return new OnlinePermData(); + } + Set formIds = allMenuList.stream() + .filter(m -> m.getOnlineFormId() != null + && m.getOnlineFlowEntryId() == null + && m.getMenuType().equals(SysMenuType.TYPE_MENU)) + .map(SysMenu::getOnlineFormId) + .collect(Collectors.toSet()); + Set viewFormIds = onlineMenuList.stream() + .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_VIEW) + .map(SysMenu::getOnlineFormId) + .collect(Collectors.toSet()); + Set editFormIds = onlineMenuList.stream() + .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_EDIT) + .map(SysMenu::getOnlineFormId) + .collect(Collectors.toSet()); + Map permDataMap = + onlineOperationService.calculatePermData(formIds, viewFormIds, editFormIds); + OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null); + permData.permUrlSet.addAll(permData.onlineWhitelistUrls); + return permData; + } + + private OnlinePermData getFlowOnlineMenuPermData(Collection allMenuList) { + List flowOnlineMenuList = allMenuList.stream() + .filter(m -> m.getOnlineFlowEntryId() != null).collect(Collectors.toList()); + Set entryIds = flowOnlineMenuList.stream() + .map(SysMenu::getOnlineFlowEntryId).collect(Collectors.toSet()); + List> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds); + List flowOnlinePermDataList = + MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class); + OnlinePermData permData = new OnlinePermData(); + flowOnlinePermDataList.forEach(perm -> { + permData.permCodeSet.addAll(perm.getPermCodeList()); + permData.permUrlSet.addAll(perm.getPermList()); + }); + return permData; + } + + static class OnlinePermData { + public final Set permCodeSet = new HashSet<>(); + public final Set permUrlSet = new HashSet<>(); + public final List onlineWhitelistUrls = new LinkedList<>(); + } + + @Data + static class OnlineFlowPermData { + private List permCodeList; + private List permList; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java new file mode 100644 index 00000000..6e57c15d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/LoginUserController.java @@ -0,0 +1,89 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.RedisKeyUtil; +import io.swagger.v3.oas.annotations.tags.Tag; +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.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +/** + * 在线用户控制器对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线用户接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/loginUser") +public class LoginUserController { + + @Autowired + private RedissonClient redissonClient; + + /** + * 显示在线用户列表。 + * + * @param loginName 登录名过滤。 + * @param pageParam 分页参数。 + * @return 登录用户信息列表。 + */ + @SaCheckPermission("loginUser.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody String loginName, @MyRequestBody MyPageParam pageParam) { + int skipCount = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + String patternKey; + if (StrUtil.isBlank(loginName)) { + patternKey = RedisKeyUtil.getSessionIdPrefix() + "*"; + } else { + patternKey = RedisKeyUtil.getSessionIdPrefix(loginName) + "*"; + } + List loginUserInfoList = new LinkedList<>(); + Iterable keys = redissonClient.getKeys().getKeysByPattern(patternKey); + for (String key : keys) { + loginUserInfoList.add(this.buildTokenDataByRedisKey(key)); + } + loginUserInfoList.sort((o1, o2) -> (int) (o2.getLoginTime().getTime() - o1.getLoginTime().getTime())); + int toIndex = Math.min(skipCount + pageParam.getPageSize(), loginUserInfoList.size()); + List resultList = loginUserInfoList.subList(skipCount, toIndex); + return ResponseResult.success(new MyPageData<>(resultList, (long) loginUserInfoList.size())); + } + + /** + * 强制下线指定登录会话。 + * + * @param sessionId 待强制下线的SessionId。 + * @return 应答结果对象。 + */ + @SaCheckPermission("loginUser.delete") + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody String sessionId) { + RBucket sessionData = redissonClient.getBucket(sessionId); + TokenData tokenData = JSON.parseObject(sessionData.get(), TokenData.class); + StpUtil.kickoutByTokenValue(tokenData.getToken()); + sessionData.delete(); + return ResponseResult.success(); + } + + private LoginUserInfo buildTokenDataByRedisKey(String key) { + RBucket sessionData = redissonClient.getBucket(key); + TokenData tokenData = JSON.parseObject(sessionData.get(), TokenData.class); + LoginUserInfo userInfo = BeanUtil.copyProperties(tokenData, LoginUserInfo.class); + userInfo.setSessionId(tokenData.getMySessionId()); + return userInfo; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java new file mode 100644 index 00000000..e389b0e7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDataPermController.java @@ -0,0 +1,337 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.alibaba.fastjson.TypeReference; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.upms.dto.SysDataPermDto; +import com.orangeforms.webadmin.upms.dto.SysUserDto; +import com.orangeforms.webadmin.upms.vo.SysDataPermVo; +import com.orangeforms.webadmin.upms.vo.SysUserVo; +import com.orangeforms.webadmin.upms.model.SysDataPerm; +import com.orangeforms.webadmin.upms.model.SysUser; +import com.orangeforms.webadmin.upms.service.SysDataPermService; +import com.orangeforms.webadmin.upms.service.SysUserService; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 数据权限接口控制器对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "数据权限管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysDataPerm") +public class SysDataPermController { + + @Autowired + private SysDataPermService sysDataPermService; + @Autowired + private SysUserService sysUserService; + + /** + * 添加新数据权限操作。 + * + * @param sysDataPermDto 新增对象。 + * @param deptIdListString 数据权限关联的部门Id列表,多个之间逗号分隔。 + * @param menuIdListString 数据权限关联的菜单Id列表,多个之间逗号分隔。 + * @return 应答结果对象。包含新增数据权限对象的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysDataPermDto.dataPermId", + "sysDataPermDto.createTimeStart", + "sysDataPermDto.createTimeEnd", + "sysDataPermDto.searchString"}) + @SaCheckPermission("sysDataPerm.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody SysDataPermDto sysDataPermDto, + @MyRequestBody String deptIdListString, + @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDataPermDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDataPerm sysDataPerm = MyModelUtil.copyTo(sysDataPermDto, SysDataPerm.class); + CallResult result = sysDataPermService.verifyRelatedData(sysDataPerm, deptIdListString, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + Set deptIdSet = null; + if (result.getData() != null) { + deptIdSet = result.getData().getObject("deptIdSet", new TypeReference>(){}); + } + sysDataPermService.saveNew(sysDataPerm, deptIdSet, menuIdSet); + return ResponseResult.success(sysDataPerm.getDataPermId()); + } + + /** + * 更新数据权限操作。 + * + * @param sysDataPermDto 更新的数据权限对象。 + * @param deptIdListString 数据权限关联的部门Id列表,多个之间逗号分隔。 + * @param menuIdListString 数据权限关联的菜单Id列表,多个之间逗号分隔。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysDataPermDto.createTimeStart", + "sysDataPermDto.createTimeEnd", + "sysDataPermDto.searchString"}) + @SaCheckPermission("sysDataPerm.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update( + @MyRequestBody SysDataPermDto sysDataPermDto, + @MyRequestBody String deptIdListString, + @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDataPermDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDataPerm originalSysDataPerm = sysDataPermService.getById(sysDataPermDto.getDataPermId()); + if (originalSysDataPerm == null) { + errorMessage = "数据验证失败,当前数据权限并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + SysDataPerm sysDataPerm = MyModelUtil.copyTo(sysDataPermDto, SysDataPerm.class); + CallResult result = sysDataPermService.verifyRelatedData(sysDataPerm, deptIdListString, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set deptIdSet = null; + if (result.getData() != null) { + deptIdSet = result.getData().getObject("deptIdSet", new TypeReference>(){}); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + if (!sysDataPermService.update(sysDataPerm, originalSysDataPerm, deptIdSet, menuIdSet)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除数据权限操作。 + * + * @param dataPermId 待删除数据权限主键Id。 + * @return 应答数据结果。 + */ + @SaCheckPermission("sysDataPerm.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long dataPermId) { + if (MyCommonUtil.existBlankArgument(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDataPermService.remove(dataPermId)) { + String errorMessage = "数据操作失败,数据权限不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 查看数据权限列表。 + * + * @param sysDataPermDtoFilter 数据权限查询过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象。包含数据权限列表。 + */ + @SaCheckPermission("sysDataPerm.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysDataPermDto sysDataPermDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysDataPerm filter = MyModelUtil.copyTo(sysDataPermDtoFilter, SysDataPerm.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysDataPerm.class); + List dataPermList = sysDataPermService.getSysDataPermListWithRelation(filter, orderBy); + List dataPermVoList = MyModelUtil.copyCollectionTo(dataPermList, SysDataPermVo.class); + long totalCount = 0L; + if (dataPermList instanceof Page) { + totalCount = ((Page) dataPermList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(dataPermVoList, totalCount)); + } + + /** + * 查看单条数据权限详情。 + * + * @param dataPermId 数据权限的主键Id。 + * @return 应答结果对象,包含数据权限的详情。 + */ + @SaCheckPermission("sysDataPerm.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long dataPermId) { + if (MyCommonUtil.existBlankArgument(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysDataPerm dataPerm = sysDataPermService.getByIdWithRelation(dataPermId, MyRelationParam.full()); + if (dataPerm == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysDataPermVo dataPermVo = MyModelUtil.copyTo(dataPerm, SysDataPermVo.class); + return ResponseResult.success(dataPermVo); + } + + /** + * 拥有指定数据权限的用户列表。 + * + * @param dataPermId 数据权限Id。 + * @param sysUserDtoFilter 用户过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysDataPerm.view") + @PostMapping("/listDataPermUser") + public ResponseResult> listDataPermUser( + @MyRequestBody Long dataPermId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doDataPermUserVerify(dataPermId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = sysUserService.getSysUserListByDataPermId(dataPermId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 获取不包含指定数据权限Id的用户列表。 + * 用户和数据权限是多对多关系,当前接口将返回没有赋值指定DataPermId的用户列表。可用于给数据权限添加新用户。 + * + * @param dataPermId 数据权限主键Id。 + * @param sysUserDtoFilter 用户数据的过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysDataPerm.update") + @PostMapping("/listNotInDataPermUser") + public ResponseResult> listNotInDataPermUser( + @MyRequestBody Long dataPermId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doDataPermUserVerify(dataPermId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = + sysUserService.getNotInSysUserListByDataPermId(dataPermId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 为指定数据权限添加用户列表。该操作可同时给一批用户赋值数据权限,并在同一事务内完成。 + * + * @param dataPermId 数据权限主键Id。 + * @param userIdListString 逗号分隔的用户Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDataPerm.update") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addDataPermUser") + public ResponseResult addDataPermUser( + @MyRequestBody Long dataPermId, @MyRequestBody String userIdListString) { + if (MyCommonUtil.existBlankArgument(dataPermId, userIdListString)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + Set userIdSet = + Arrays.stream(userIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysDataPermService.existId(dataPermId) + || !sysUserService.existUniqueKeyList("userId", userIdSet)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + sysDataPermService.addDataPermUserList(dataPermId, userIdSet); + return ResponseResult.success(); + } + + /** + * 为指定用户移除指定数据权限。 + * + * @param dataPermId 指定数据权限主键Id。 + * @param userId 指定用户主键Id。 + * @return 应答数据结果。 + */ + @SaCheckPermission("sysDataPerm.update") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteDataPermUser") + public ResponseResult deleteDataPermUser( + @MyRequestBody Long dataPermId, @MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(dataPermId, userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDataPermService.removeDataPermUser(dataPermId, userId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 以字典形式返回全部数据权限管理数据集合。字典的键值为[dataPermId, dataPermName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysDataPermDto filter) { + List resultList = + sysDataPermService.getListByFilter(MyModelUtil.copyTo(filter, SysDataPerm.class)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysDataPerm::getDataPermId, SysDataPerm::getDataPermName)); + } + + private ResponseResult doDataPermUserVerify(Long dataPermId) { + if (MyCommonUtil.existBlankArgument(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDataPermService.existId(dataPermId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java new file mode 100644 index 00000000..3c1fb0f8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysDeptController.java @@ -0,0 +1,428 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ObjectUtil; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.vo.*; +import com.orangeforms.webadmin.upms.dto.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 部门管理操作控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "部门管理管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysDept") +public class SysDeptController { + + @Autowired + private SysPostService sysPostService; + @Autowired + private SysDeptService sysDeptService; + + /** + * 新增部门管理数据。 + * + * @param sysDeptDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysDeptDto.deptId"}) + @SaCheckPermission("sysDept.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody SysDeptDto sysDeptDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDept sysDept = MyModelUtil.copyTo(sysDeptDto, SysDept.class); + // 验证父Id的数据合法性 + SysDept parentSysDept = null; + if (MyCommonUtil.isNotBlankOrNull(sysDept.getParentId())) { + parentSysDept = sysDeptService.getById(sysDept.getParentId()); + if (parentSysDept == null) { + errorMessage = "数据验证失败,关联的父节点并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_PARENT_ID_NOT_EXIST, errorMessage); + } + } + sysDept = sysDeptService.saveNew(sysDept, parentSysDept); + return ResponseResult.success(sysDept.getDeptId()); + } + + /** + * 更新部门管理数据。 + * + * @param sysDeptDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody SysDeptDto sysDeptDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDept sysDept = MyModelUtil.copyTo(sysDeptDto, SysDept.class); + SysDept originalSysDept = sysDeptService.getById(sysDept.getDeptId()); + if (originalSysDept == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + // 验证父Id的数据合法性 + if (MyCommonUtil.isNotBlankOrNull(sysDept.getParentId()) + && ObjectUtil.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) { + SysDept parentSysDept = sysDeptService.getById(sysDept.getParentId()); + if (parentSysDept == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,关联的 [父节点] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_PARENT_ID_NOT_EXIST, errorMessage); + } + } + if (!sysDeptService.update(sysDept, originalSysDept)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除部门管理数据。 + * + * @param deptId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long deptId) { + if (MyCommonUtil.existBlankArgument(deptId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + return this.doDelete(deptId); + } + + /** + * 批量删除部门管理数据。 + * + * @param deptIdList 待删除对象的主键Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.delete") + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatch") + public ResponseResult deleteBatch(@MyRequestBody List deptIdList) { + if (MyCommonUtil.existBlankArgument(deptIdList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + for (Long deptId : deptIdList) { + ResponseResult responseResult = this.doDelete(deptId); + if (!responseResult.isSuccess()) { + return responseResult; + } + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的部门管理列表。 + * + * @param sysDeptDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("sysDept.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysDeptDto sysDeptDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + SysDept sysDeptFilter = MyModelUtil.copyTo(sysDeptDtoFilter, SysDept.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysDept.class); + List sysDeptList = sysDeptService.getSysDeptListWithRelation(sysDeptFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysDeptList, SysDeptVo.class)); + } + + /** + * 查看指定部门管理对象详情。 + * + * @param deptId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("sysDept.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long deptId) { + SysDept sysDept = sysDeptService.getByIdWithRelation(deptId, MyRelationParam.full()); + if (sysDept == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysDeptVo sysDeptVo = MyModelUtil.copyTo(sysDept, SysDeptVo.class); + return ResponseResult.success(sysDeptVo); + } + + /** + * 列出不与指定部门管理存在多对多关系的 [岗位管理] 列表数据。通常用于查看添加新 [岗位管理] 对象的候选列表。 + * + * @param deptId 主表关联字段。 + * @param sysPostDtoFilter [岗位管理] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/listNotInSysDeptPost") + public ResponseResult> listNotInSysDeptPost( + @MyRequestBody Long deptId, + @MyRequestBody SysPostDto sysPostDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (MyCommonUtil.isNotBlankOrNull(deptId) && !sysDeptService.existId(deptId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysPost filter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class); + List sysPostList; + if (MyCommonUtil.isNotBlankOrNull(deptId)) { + sysPostList = sysPostService.getNotInSysPostListByDeptId(deptId, filter, orderBy); + } else { + sysPostList = sysPostService.getSysPostList(filter, orderBy); + sysPostService.buildRelationForDataList(sysPostList, MyRelationParam.dictOnly()); + } + return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class)); + } + + /** + * 列出与指定部门管理存在多对多关系的 [岗位管理] 列表数据。 + * + * @param deptId 主表关联字段。 + * @param sysPostDtoFilter [岗位管理] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("sysDept.view") + @PostMapping("/listSysDeptPost") + public ResponseResult> listSysDeptPost( + @MyRequestBody(required = true) Long deptId, + @MyRequestBody SysPostDto sysPostDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (!sysDeptService.existId(deptId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysPost filter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class); + List sysPostList = sysPostService.getSysPostListByDeptId(deptId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class)); + } + + /** + * 批量添加部门管理和 [岗位管理] 对象的多对多关联关系数据。 + * + * @param deptId 主表主键Id。 + * @param sysDeptPostDtoList 关联对象列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/addSysDeptPost") + public ResponseResult addSysDeptPost( + @MyRequestBody Long deptId, + @MyRequestBody List sysDeptPostDtoList) { + if (MyCommonUtil.existBlankArgument(deptId, sysDeptPostDtoList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptPostDtoList); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Set postIdSet = sysDeptPostDtoList.stream().map(SysDeptPostDto::getPostId).collect(Collectors.toSet()); + if (!sysDeptService.existId(deptId) || !sysPostService.existUniqueKeyList("postId", postIdSet)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + List sysDeptPostList = MyModelUtil.copyCollectionTo(sysDeptPostDtoList, SysDeptPost.class); + sysDeptService.addSysDeptPostList(sysDeptPostList, deptId); + return ResponseResult.success(); + } + + /** + * 更新指定部门管理和指定 [岗位管理] 的多对多关联数据。 + * + * @param sysDeptPostDto 对多对中间表对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/updateSysDeptPost") + public ResponseResult updateSysDeptPost(@MyRequestBody SysDeptPostDto sysDeptPostDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysDeptPostDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysDeptPost sysDeptPost = MyModelUtil.copyTo(sysDeptPostDto, SysDeptPost.class); + if (!sysDeptService.updateSysDeptPost(sysDeptPost)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 显示部门管理和指定 [岗位管理] 的多对多关联详情数据。 + * + * @param deptId 主表主键Id。 + * @param postId 从表主键Id。 + * @return 应答结果对象,包括中间表详情。 + */ + @SaCheckPermission("sysDept.update") + @GetMapping("/viewSysDeptPost") + public ResponseResult viewSysDeptPost(@RequestParam Long deptId, @RequestParam Long postId) { + SysDeptPost sysDeptPost = sysDeptService.getSysDeptPost(deptId, postId); + if (sysDeptPost == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysDeptPostVo sysDeptPostVo = MyModelUtil.copyTo(sysDeptPost, SysDeptPostVo.class); + return ResponseResult.success(sysDeptPostVo); + } + + /** + * 移除指定部门管理和指定 [岗位管理] 的多对多关联关系。 + * + * @param deptId 主表主键Id。 + * @param postId 从表主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysDept.update") + @PostMapping("/deleteSysDeptPost") + public ResponseResult deleteSysDeptPost(@MyRequestBody Long deptId, @MyRequestBody Long postId) { + if (MyCommonUtil.existBlankArgument(deptId, postId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysDeptService.removeSysDeptPost(deptId, postId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 获取部门岗位多对多关联数据,及其关联的部门和岗位数据。 + * + * @param deptId 部门Id,如果为空,返回全部数据列表。 + * @return 部门岗位多对多关联数据,及其关联的部门和岗位数据 + */ + @GetMapping("/listSysDeptPostWithRelation") + public ResponseResult>> listSysDeptPostWithRelation( + @RequestParam(required = false) Long deptId) { + return ResponseResult.success(sysDeptService.getSysDeptPostListWithRelationByDeptId(deptId)); + } + + /** + * 以字典形式返回全部部门管理数据集合。字典的键值为[deptId, deptName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysDeptDto filter) { + List resultList = + sysDeptService.getListByFilter(MyModelUtil.copyTo(filter, SysDept.class)); + return ResponseResult.success(MyCommonUtil.toDictDataList( + resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = sysDeptService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success(MyCommonUtil.toDictDataList( + resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId)); + } + + /** + * 根据父主键Id,以字典的形式返回其下级数据列表。 + * 白名单接口,登录用户均可访问。 + * + * @param parentId 父主键Id。 + * @return 按照字典的形式返回下级数据列表。 + */ + @GetMapping("/listDictByParentId") + public ResponseResult>> listDictByParentId(@RequestParam(required = false) Long parentId) { + List resultList = sysDeptService.getListByParentId("parentId", parentId); + return ResponseResult.success(MyCommonUtil.toDictDataList( + resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId)); + } + + /** + * 根据父主键Id列表,获取当前部门Id及其所有下级部门Id列表。 + * 白名单接口,登录用户均可访问。 + * + * @param parentIds 父主键Id列表,多个Id之间逗号分隔。 + * @return 获取当前部门Id及其所有下级部门Id列表。 + */ + @GetMapping("/listAllChildDeptIdByParentIds") + public ResponseResult> listAllChildDeptIdByParentIds( + @RequestParam(required = false) String parentIds) { + List parentIdList = StrUtil.split(parentIds, ',') + .stream().map(Long::valueOf).collect(Collectors.toList()); + return ResponseResult.success(sysDeptService.getAllChildDeptIdByParentIds(parentIdList)); + } + + private ResponseResult doDelete(Long deptId) { + String errorMessage; + // 验证关联Id的数据合法性 + SysDept originalSysDept = sysDeptService.getById(deptId); + if (originalSysDept == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (sysDeptService.hasChildren(deptId)) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象存在子对象] ,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage); + } + if (sysDeptService.hasChildrenUser(deptId)) { + errorMessage = "数据验证失败,请先移除部门用户数据后,再删除当前部门!"; + return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage); + } + if (!sysDeptService.remove(deptId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java new file mode 100644 index 00000000..0ea5f339 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysMenuController.java @@ -0,0 +1,231 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.upms.dto.SysMenuDto; +import com.orangeforms.webadmin.upms.vo.SysMenuVo; +import com.orangeforms.webadmin.upms.model.SysMenu; +import com.orangeforms.webadmin.upms.model.SysDataPerm; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import com.orangeforms.webadmin.upms.service.SysMenuService; +import com.orangeforms.webadmin.upms.service.SysDataPermService; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 菜单管理接口控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "菜单管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysMenu") +public class SysMenuController { + + @Autowired + private SysMenuService sysMenuService; + @Autowired + private SysDataPermService sysDataPermService; + + /** + * 添加新菜单操作。 + * + * @param sysMenuDto 新菜单对象。 + * @return 应答结果对象,包含新增菜单的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysMenuDto.menuId"}) + @SaCheckPermission("sysMenu.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody SysMenuDto sysMenuDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysMenuDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysMenu sysMenu = MyModelUtil.copyTo(sysMenuDto, SysMenu.class); + if (sysMenu.getParentId() != null) { + SysMenu parentSysMenu = sysMenuService.getById(sysMenu.getParentId()); + if (parentSysMenu == null) { + errorMessage = "数据验证失败,关联的父菜单不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (parentSysMenu.getOnlineFormId() != null) { + errorMessage = "数据验证失败,不能为动态表单菜单添加子菜单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + CallResult result = sysMenuService.verifyRelatedData(sysMenu, null); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + sysMenuService.saveNew(sysMenu); + return ResponseResult.success(sysMenu.getMenuId()); + } + + /** + * 更新菜单数据操作。 + * + * @param sysMenuDto 新菜单对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysMenu.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody SysMenuDto sysMenuDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysMenuDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysMenu originalSysMenu = sysMenuService.getById(sysMenuDto.getMenuId()); + if (originalSysMenu == null) { + errorMessage = "数据验证失败,当前菜单并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + SysMenu sysMenu = MyModelUtil.copyTo(sysMenuDto, SysMenu.class); + if (ObjectUtil.notEqual(originalSysMenu.getOnlineFormId(), sysMenu.getOnlineFormId())) { + if (originalSysMenu.getOnlineFormId() == null) { + errorMessage = "数据验证失败,不能为当前菜单添加在线表单Id属性!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (sysMenu.getOnlineFormId() == null) { + errorMessage = "数据验证失败,不能去掉当前菜单的在线表单Id属性!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + if (originalSysMenu.getOnlineFormId() != null + && originalSysMenu.getMenuType().equals(SysMenuType.TYPE_BUTTON)) { + errorMessage = "数据验证失败,在线表单的内置菜单不能编辑!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult result = sysMenuService.verifyRelatedData(sysMenu, originalSysMenu); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + if (!sysMenuService.update(sysMenu, originalSysMenu)) { + errorMessage = "数据验证失败,当前权限字并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除指定菜单操作。 + * + * @param menuId 指定菜单主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysMenu.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long menuId) { + if (MyCommonUtil.existBlankArgument(menuId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + String errorMessage; + SysMenu menu = sysMenuService.getById(menuId); + if (menu == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (menu.getOnlineFormId() != null && menu.getMenuType().equals(SysMenuType.TYPE_BUTTON)) { + errorMessage = "数据验证失败,在线表单的内置菜单不能删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 对于在线表单,无需进行子菜单的验证,而是在删除的时候,连同子菜单一起删除。 + if (menu.getOnlineFormId() == null && sysMenuService.hasChildren(menuId)) { + errorMessage = "数据验证失败,当前菜单存在下级菜单!"; + return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage); + } + List dataPermList = sysDataPermService.getSysDataPermListByMenuId(menuId); + if (CollUtil.isNotEmpty(dataPermList)) { + SysDataPerm dataPerm = dataPermList.get(0); + errorMessage = "数据验证失败,当前菜单正在被数据权限 [" + dataPerm.getDataPermName() + "] 引用,不能直接删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!sysMenuService.remove(menu)) { + errorMessage = "数据操作失败,菜单不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 获取全部菜单列表。 + * + * @return 应答结果对象,包含全部菜单数据列表。 + */ + @SaCheckPermission("sysMenu.view") + @PostMapping("/list") + public ResponseResult> list() { + List resultList = this.getAllMenuListByShowOrder(); + return ResponseResult.success(MyModelUtil.copyCollectionTo(resultList, SysMenuVo.class)); + } + + /** + * 查看指定菜单数据详情。 + * + * @param menuId 指定菜单主键Id。 + * @return 应答结果对象,包含菜单详情。 + */ + @SaCheckPermission("sysMenu.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long menuId) { + if (MyCommonUtil.existBlankArgument(menuId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysMenu sysMenu = sysMenuService.getByIdWithRelation(menuId, MyRelationParam.full()); + if (sysMenu == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysMenuVo sysMenuVo = MyModelUtil.copyTo(sysMenu, SysMenuVo.class); + return ResponseResult.success(sysMenuVo); + } + + /** + * 以字典形式返回目录和菜单类型的菜单管理数据集合。字典的键值为[menuId, menuName]。 + * 白名单接口,登录用户均可访问。 + * + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listMenuDict") + public ResponseResult>> listMenuDict() { + List resultList = this.getAllMenuListByShowOrder(); + resultList = resultList.stream() + .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList()); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId)); + } + + /** + * 以字典形式返回全部的菜单管理数据集合。字典的键值为[menuId, menuName]。 + * 白名单接口,登录用户均可访问。 + * + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict() { + List resultList = this.getAllMenuListByShowOrder(); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId)); + } + + private List getAllMenuListByShowOrder() { + return sysMenuService.getAllListByOrder("showOrder"); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java new file mode 100644 index 00000000..d7ec940f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysOperationLogController.java @@ -0,0 +1,63 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.log.model.SysOperationLog; +import com.orangeforms.common.log.service.SysOperationLogService; +import com.orangeforms.common.log.dto.SysOperationLogDto; +import com.orangeforms.common.log.vo.SysOperationLogVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 操作日志接口控制器对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "操作日志接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysOperationLog") +public class SysOperationLogController { + + @Autowired + private SysOperationLogService operationLogService; + + /** + * 数据权限列表。 + * + * @param sysOperationLogDtoFilter 操作日志查询过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象。包含操作日志列表。 + */ + @SaCheckPermission("sysOperationLog.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysOperationLogDto sysOperationLogDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysOperationLog filter = MyModelUtil.copyTo(sysOperationLogDtoFilter, SysOperationLog.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysOperationLog.class); + List operationLogList = operationLogService.getSysOperationLogList(filter, orderBy); + List operationLogVoList = MyModelUtil.copyCollectionTo(operationLogList, SysOperationLogVo.class); + long totalCount = 0L; + if (operationLogList instanceof Page) { + totalCount = ((Page) operationLogList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(operationLogVoList, totalCount)); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java new file mode 100644 index 00000000..9f4dcec4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysPostController.java @@ -0,0 +1,183 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.webadmin.upms.dto.SysPostDto; +import com.orangeforms.webadmin.upms.model.SysPost; +import com.orangeforms.webadmin.upms.service.SysPostService; +import com.orangeforms.webadmin.upms.vo.SysPostVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import jakarta.validation.groups.Default; + +/** + * 岗位管理操作控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "岗位管理操作管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysPost") +public class SysPostController { + + @Autowired + private SysPostService sysPostService; + + /** + * 新增岗位管理数据。 + * + * @param sysPostDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysPostDto.postId"}) + @SaCheckPermission("sysPost.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody SysPostDto sysPostDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysPostDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysPost sysPost = MyModelUtil.copyTo(sysPostDto, SysPost.class); + sysPost = sysPostService.saveNew(sysPost); + return ResponseResult.success(sysPost.getPostId()); + } + + /** + * 更新岗位管理数据。 + * + * @param sysPostDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysPost.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody SysPostDto sysPostDto) { + String errorMessage = MyCommonUtil.getModelValidationError(sysPostDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysPost sysPost = MyModelUtil.copyTo(sysPostDto, SysPost.class); + SysPost originalSysPost = sysPostService.getById(sysPost.getPostId()); + if (originalSysPost == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!sysPostService.update(sysPost, originalSysPost)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除岗位管理数据。 + * + * @param postId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysPost.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long postId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(postId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + SysPost originalSysPost = sysPostService.getById(postId); + if (originalSysPost == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!sysPostService.remove(postId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的岗位管理列表。 + * + * @param sysPostDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("sysPost.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysPostDto sysPostDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysPost sysPostFilter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class); + List sysPostList = sysPostService.getSysPostListWithRelation(sysPostFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class)); + } + + /** + * 查看指定岗位管理对象详情。 + * + * @param postId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("sysPost.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long postId) { + if (MyCommonUtil.existBlankArgument(postId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysPost sysPost = sysPostService.getByIdWithRelation(postId, MyRelationParam.full()); + if (sysPost == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysPostVo sysPostVo = MyModelUtil.copyTo(sysPost, SysPostVo.class); + return ResponseResult.success(sysPostVo); + } + + /** + * 以字典形式返回全部岗位管理数据集合。字典的键值为[postId, postName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysPostDto filter) { + List resultList = sysPostService.getListByFilter(MyModelUtil.copyTo(filter, SysPost.class)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysPost::getPostId, SysPost::getPostName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param postIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List postIds) { + List resultList = sysPostService.getInList(new HashSet<>(postIds)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysPost::getPostId, SysPost::getPostName)); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java new file mode 100644 index 00000000..25e5c51f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysRoleController.java @@ -0,0 +1,331 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.alibaba.fastjson.TypeReference; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.webadmin.upms.dto.SysRoleDto; +import com.orangeforms.webadmin.upms.dto.SysUserDto; +import com.orangeforms.webadmin.upms.vo.SysRoleVo; +import com.orangeforms.webadmin.upms.vo.SysUserVo; +import com.orangeforms.webadmin.upms.model.SysRole; +import com.orangeforms.webadmin.upms.model.SysUser; +import com.orangeforms.webadmin.upms.model.SysUserRole; +import com.orangeforms.webadmin.upms.service.SysRoleService; +import com.orangeforms.webadmin.upms.service.SysUserService; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 角色管理接口控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "角色管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysRole") +public class SysRoleController { + + @Autowired + private SysRoleService sysRoleService; + @Autowired + private SysUserService sysUserService; + + /** + * 新增角色操作。 + * + * @param sysRoleDto 新增角色对象。 + * @param menuIdListString 与当前角色Id绑定的menuId列表,多个menuId之间逗号分隔。 + * @return 应答结果对象,包含新增角色的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"sysRoleDto.roleId", "sysRoleDto.createTimeStart", "sysRoleDto.createTimeEnd"}) + @SaCheckPermission("sysRole.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody SysRoleDto sysRoleDto, @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysRoleDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysRole sysRole = MyModelUtil.copyTo(sysRoleDto, SysRole.class); + CallResult result = sysRoleService.verifyRelatedData(sysRole, null, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + sysRoleService.saveNew(sysRole, menuIdSet); + return ResponseResult.success(sysRole.getRoleId()); + } + + /** + * 更新角色操作。 + * + * @param sysRoleDto 更新角色对象。 + * @param menuIdListString 与当前角色Id绑定的menuId列表,多个menuId之间逗号分隔。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = {"sysRoleDto.createTimeStart", "sysRoleDto.createTimeEnd"}) + @SaCheckPermission("sysRole.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update( + @MyRequestBody SysRoleDto sysRoleDto, @MyRequestBody String menuIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysRoleDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysRole originalSysRole = sysRoleService.getById(sysRoleDto.getRoleId()); + if (originalSysRole == null) { + errorMessage = "数据验证失败,当前角色并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + SysRole sysRole = MyModelUtil.copyTo(sysRoleDto, SysRole.class); + CallResult result = sysRoleService.verifyRelatedData(sysRole, originalSysRole, menuIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set menuIdSet = null; + if (result.getData() != null) { + menuIdSet = result.getData().getObject("menuIdSet", new TypeReference>(){}); + } + if (!sysRoleService.update(sysRole, originalSysRole, menuIdSet)) { + errorMessage = "更新失败,数据不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除指定角色操作。 + * + * @param roleId 指定角色主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysRole.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long roleId) { + if (MyCommonUtil.existBlankArgument(roleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysRoleService.remove(roleId)) { + String errorMessage = "数据操作失败,角色不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 查看角色列表。 + * + * @param sysRoleDtoFilter 角色过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含角色列表。 + */ + @SaCheckPermission("sysRole.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysRoleDto sysRoleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysRole filter = MyModelUtil.copyTo(sysRoleDtoFilter, SysRole.class); + List roleList = sysRoleService.getSysRoleList( + filter, MyOrderParam.buildOrderBy(orderParam, SysRole.class)); + List roleVoList = MyModelUtil.copyCollectionTo(roleList, SysRoleVo.class); + long totalCount = 0L; + if (roleList instanceof Page) { + totalCount = ((Page) roleList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(roleVoList, totalCount)); + } + + /** + * 查看角色详情。 + * + * @param roleId 指定角色主键Id。 + * @return 应答结果对象,包含角色详情对象。 + */ + @SaCheckPermission("sysRole.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long roleId) { + if (MyCommonUtil.existBlankArgument(roleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + SysRole sysRole = sysRoleService.getByIdWithRelation(roleId, MyRelationParam.full()); + if (sysRole == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysRoleVo sysRoleVo = MyModelUtil.copyTo(sysRole, SysRoleVo.class); + return ResponseResult.success(sysRoleVo); + } + + /** + * 拥有指定角色的用户列表。 + * + * @param roleId 角色主键Id。 + * @param sysUserDtoFilter 用户过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysRole.view") + @PostMapping("/listUserRole") + public ResponseResult> listUserRole( + @MyRequestBody Long roleId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doRoleUserVerify(roleId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = sysUserService.getSysUserListByRoleId(roleId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 获取不包含指定角色Id的用户列表。 + * 用户和角色是多对多关系,当前接口将返回没有赋值指定RoleId的用户列表。可用于给角色添加新用户。 + * + * @param roleId 角色主键Id。 + * @param sysUserDtoFilter 用户过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含用户列表数据。 + */ + @SaCheckPermission("sysRole.update") + @PostMapping("/listNotInUserRole") + public ResponseResult> listNotInUserRole( + @MyRequestBody Long roleId, + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doRoleUserVerify(roleId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List userList = sysUserService.getNotInSysUserListByRoleId(roleId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class)); + } + + /** + * 为指定角色添加用户列表。该操作可同时给一批用户赋值角色,并在同一事务内完成。 + * + * @param roleId 角色主键Id。 + * @param userIdListString 逗号分隔的用户Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysRole.update") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addUserRole") + public ResponseResult addUserRole(@MyRequestBody Long roleId, @MyRequestBody String userIdListString) { + if (MyCommonUtil.existBlankArgument(roleId, userIdListString)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + Set userIdSet = Arrays.stream( + userIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysRoleService.existId(roleId) + || !sysUserService.existUniqueKeyList("userId", userIdSet)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + List userRoleList = new LinkedList<>(); + for (Long userId : userIdSet) { + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(roleId); + userRole.setUserId(userId); + userRoleList.add(userRole); + } + sysRoleService.addUserRoleList(userRoleList); + return ResponseResult.success(); + } + + /** + * 为指定用户移除指定角色。 + * + * @param roleId 指定角色主键Id。 + * @param userId 指定用户主键Id。 + * @return 应答数据结果。 + */ + @SaCheckPermission("sysRole.update") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteUserRole") + public ResponseResult deleteUserRole(@MyRequestBody Long roleId, @MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(roleId, userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysRoleService.removeUserRole(roleId, userId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 以字典形式返回全部角色管理数据集合。字典的键值为[roleId, roleName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysRoleDto filter) { + List resultList = sysRoleService.getListByFilter(MyModelUtil.copyTo(filter, SysRole.class)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysRole::getRoleId, SysRole::getRoleName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = sysRoleService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysRole::getRoleId, SysRole::getRoleName)); + } + + private ResponseResult doRoleUserVerify(Long roleId) { + if (MyCommonUtil.existBlankArgument(roleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysRoleService.existId(roleId)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java new file mode 100644 index 00000000..406898d2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/controller/SysUserController.java @@ -0,0 +1,378 @@ +package com.orangeforms.webadmin.upms.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.alibaba.fastjson.TypeReference; +import cn.hutool.core.util.ReflectUtil; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreInfo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.vo.*; +import com.orangeforms.webadmin.upms.dto.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.webadmin.config.ApplicationConfig; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * 用户管理操作控制器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "用户管理管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/upms/sysUser") +public class SysUserController { + + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private ApplicationConfig appConfig; + @Autowired + private SessionCacheHelper cacheHelper; + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private SysUserService sysUserService; + + /** + * 新增用户操作。 + * + * @param sysUserDto 新增用户对象。 + * @param deptPostIdListString 逗号分隔的部门岗位Id列表。 + * @param dataPermIdListString 逗号分隔的数据权限Id列表。 + * @param roleIdListString 逗号分隔的角色Id列表。 + * @return 应答结果对象,包含新增用户的主键Id。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysUserDto.userId", + "sysUserDto.createTimeStart", + "sysUserDto.createTimeEnd"}) + @SaCheckPermission("sysUser.add") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody SysUserDto sysUserDto, + @MyRequestBody String deptPostIdListString, + @MyRequestBody String dataPermIdListString, + @MyRequestBody String roleIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysUserDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysUser sysUser = MyModelUtil.copyTo(sysUserDto, SysUser.class); + CallResult result = sysUserService.verifyRelatedData( + sysUser, null, roleIdListString, deptPostIdListString, dataPermIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set deptPostIdSet = result.getData().getObject("deptPostIdSet", new TypeReference>() {}); + Set roleIdSet = result.getData().getObject("roleIdSet", new TypeReference>() {}); + Set dataPermIdSet = result.getData().getObject("dataPermIdSet", new TypeReference>() {}); + sysUserService.saveNew(sysUser, roleIdSet, deptPostIdSet, dataPermIdSet); + return ResponseResult.success(sysUser.getUserId()); + } + + /** + * 更新用户操作。 + * + * @param sysUserDto 更新用户对象。 + * @param deptPostIdListString 逗号分隔的部门岗位Id列表。 + * @param dataPermIdListString 逗号分隔的数据权限Id列表。 + * @param roleIdListString 逗号分隔的角色Id列表。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = { + "sysUserDto.createTimeStart", + "sysUserDto.createTimeEnd"}) + @SaCheckPermission("sysUser.update") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update( + @MyRequestBody SysUserDto sysUserDto, + @MyRequestBody String deptPostIdListString, + @MyRequestBody String dataPermIdListString, + @MyRequestBody String roleIdListString) { + String errorMessage = MyCommonUtil.getModelValidationError(sysUserDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SysUser originalUser = sysUserService.getById(sysUserDto.getUserId()); + if (originalUser == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysUser sysUser = MyModelUtil.copyTo(sysUserDto, SysUser.class); + CallResult result = sysUserService.verifyRelatedData( + sysUser, originalUser, roleIdListString, deptPostIdListString, dataPermIdListString); + if (!result.isSuccess()) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + Set roleIdSet = result.getData().getObject("roleIdSet", new TypeReference>() {}); + Set deptPostIdSet = result.getData().getObject("deptPostIdSet", new TypeReference>() {}); + Set dataPermIdSet = result.getData().getObject("dataPermIdSet", new TypeReference>() {}); + if (!sysUserService.update(sysUser, originalUser, roleIdSet, deptPostIdSet, dataPermIdSet)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 重置密码操作。 + * + * @param userId 指定用户主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysUser.resetPassword") + @PostMapping("/resetPassword") + public ResponseResult resetPassword(@MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!sysUserService.changePassword(userId, appConfig.getDefaultUserPassword())) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除用户管理数据。 + * + * @param userId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysUser.delete") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long userId) { + if (MyCommonUtil.existBlankArgument(userId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + return this.doDelete(userId); + } + + /** + * 批量删除用户管理数据。 + * + * @param userIdList 待删除对象的主键Id列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("sysUser.delete") + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatch") + public ResponseResult deleteBatch(@MyRequestBody List userIdList) { + if (MyCommonUtil.existBlankArgument(userIdList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + for (Long userId : userIdList) { + ResponseResult responseResult = this.doDelete(userId); + if (!responseResult.isSuccess()) { + return responseResult; + } + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的用户管理列表。 + * + * @param sysUserDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("sysUser.view") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody SysUserDto sysUserDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + SysUser sysUserFilter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class); + List sysUserList = sysUserService.getSysUserListWithRelation(sysUserFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(sysUserList, SysUserVo.class)); + } + + /** + * 查看指定用户管理对象详情。 + * + * @param userId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("sysUser.view") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long userId) { + // 这里查看用户数据时候,需要把用户多对多关联的角色和数据权限Id一并查出。 + SysUser sysUser = sysUserService.getByIdWithRelation(userId, MyRelationParam.full()); + if (sysUser == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + SysUserVo sysUserVo = MyModelUtil.copyTo(sysUser, SysUserVo.class); + return ResponseResult.success(sysUserVo); + } + + /** + * 附件文件下载。 + * 这里将图片和其他类型的附件文件放到不同的父目录下,主要为了便于今后图片文件的迁移。 + * + * @param userId 附件所在记录的主键Id。 + * @param fieldName 附件所属的字段名。 + * @param filename 文件名。如果没有提供该参数,就从当前记录的指定字段中读取。 + * @param asImage 下载文件是否为图片。 + * @param response Http 应答对象。 + */ + @SaCheckPermission("sysUser.view") + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/download") + public void download( + @RequestParam(required = false) Long userId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) { + if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。 + // 否则有可能给前端返回的是200的错误码。 + try { + // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。 + if (userId == null) { + if (!cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } else { + SysUser sysUser = sysUserService.getById(userId); + if (sysUser == null) { + ResponseResult.output(HttpServletResponse.SC_NOT_FOUND); + return; + } + String fieldJsonData = (String) ReflectUtil.getFieldValue(sysUser, fieldName); + if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST); + return; + } + if (!BaseUpDownloader.containFile(fieldJsonData, filename) + && !cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, fieldName); + if (!storeInfo.isSupportUpload()) { + ResponseResult.output(HttpServletResponse.SC_NOT_IMPLEMENTED, + ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD)); + return; + } + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + upDownloader.doDownload(appConfig.getUploadFileBaseDir(), + SysUser.class.getSimpleName(), fieldName, filename, asImage, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + /** + * 文件上传操作。 + * + * @param fieldName 上传文件名。 + * @param asImage 是否作为图片上传。如果是图片,今后下载的时候无需权限验证。否则就是附件上传,下载时需要权限验证。 + * @param uploadFile 上传文件对象。 + */ + @SaCheckPermission("sysUser.view") + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/upload") + public void upload( + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, fieldName); + // 这里就会判断参数中指定的字段,是否支持上传操作。 + if (!storeInfo.isSupportUpload()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD)); + return; + } + // 根据字段注解中的存储类型,通过工厂方法获取匹配的上传下载实现类,从而解耦。 + BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType()); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), fieldName, asImage, uploadFile); + if (Boolean.TRUE.equals(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + cacheHelper.putSessionUploadFile(responseInfo.getFilename()); + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + /** + * 以字典形式返回全部用户管理数据集合。字典的键值为[userId, showName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject SysUserDto filter) { + List resultList = + sysUserService.getListByFilter(MyModelUtil.copyTo(filter, SysUser.class)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysUser::getUserId, SysUser::getShowName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = sysUserService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, SysUser::getUserId, SysUser::getShowName)); + } + + private ResponseResult doDelete(Long userId) { + String errorMessage; + // 验证关联Id的数据合法性 + SysUser originalSysUser = sysUserService.getById(userId); + if (originalSysUser == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!sysUserService.remove(userId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java new file mode 100644 index 00000000..db58a68f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermDeptMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPermDept; + +/** + * 数据权限与部门关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermDeptMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java new file mode 100644 index 00000000..9483f952 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMapper.java @@ -0,0 +1,43 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPerm; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据权限数据访问操作接口。 + * NOTE: 该对象一定不能被 @EnableDataPerm 注解标注,否则会导致无限递归。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermMapper extends BaseDaoMapper { + + /** + * 获取数据权限列表。 + * + * @param sysDataPermFilter 过滤对象。 + * @param orderBy 排序字符串。 + * @return 过滤后的数据权限列表。 + */ + List getSysDataPermList( + @Param("sysDataPermFilter") SysDataPerm sysDataPermFilter, @Param("orderBy") String orderBy); + + /** + * 获取指定用户的数据权限列表。 + * + * @param userId 用户Id。 + * @return 数据权限列表。 + */ + List getSysDataPermListByUserId(@Param("userId") Long userId); + + /** + * 查询与指定菜单关联的数据权限列表。 + * + * @param menuId 菜单Id。 + * @return 与菜单Id关联的数据权限列表。 + */ + List getSysDataPermListByMenuId(@Param("menuId") Long menuId); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java new file mode 100644 index 00000000..37fa8274 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermMenuMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPermMenu; + +/** + * 数据权限与菜单关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermMenuMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java new file mode 100644 index 00000000..1ca7d6d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDataPermUserMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDataPermUser; + +/** + * 数据权限与用户关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermUserMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java new file mode 100644 index 00000000..9f0dc2c2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptMapper.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDept; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 部门管理数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptMapper extends BaseDaoMapper { + + /** + * 批量插入对象列表。 + * + * @param sysDeptList 新增对象列表。 + */ + void insertList(List sysDeptList); + + /** + * 获取过滤后的对象列表。 + * + * @param sysDeptFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysDeptList( + @Param("sysDeptFilter") SysDept sysDeptFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java new file mode 100644 index 00000000..93eb328a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptPostMapper.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDeptPost; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 部门岗位数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptPostMapper extends BaseDaoMapper { + + /** + * 获取指定部门Id的部门岗位多对多关联数据列表,以及关联的部门和岗位数据。 + * + * @param deptId 部门Id。如果参数为空则返回全部数据。 + * @return 部门岗位多对多数据列表。 + */ + List> getSysDeptPostListWithRelationByDeptId(@Param("deptId") Long deptId); + + /** + * 获取指定部门Id的领导部门岗位列表。 + * + * @param deptId 部门Id。 + * @return 指定部门Id的领导部门岗位列表 + */ + List getLeaderDeptPostList(@Param("deptId") Long deptId); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java new file mode 100644 index 00000000..a0f66281 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysDeptRelationMapper.java @@ -0,0 +1,42 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysDeptRelation; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 部门关系树关联关系表访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptRelationMapper extends BaseDaoMapper { + + /** + * 将myDeptId的所有子部门,与其父部门parentDeptId解除关联关系。 + * + * @param parentDeptIds myDeptId的父部门Id列表。 + * @param myDeptId 当前部门。 + */ + void removeBetweenChildrenAndParents( + @Param("parentDeptIds") List parentDeptIds, @Param("myDeptId") Long myDeptId); + + /** + * 批量插入部门关联数据。 + * 由于目前版本(3.4.1)的Mybatis Plus没有提供真正的批量插入,为了保证效率需要自己实现。 + * 目前我们仅仅给出MySQL和PostgresSQL的insert list实现作为参考,其他数据库需要自行修改。 + * + * @param deptRelationList 部门关联关系数据列表。 + */ + void insertList(List deptRelationList); + + /** + * 批量插入当前部门的所有父部门列表,包括自己和自己的关系。 + * + * @param parentDeptId myDeptId的父部门Id。 + * @param myDeptId 当前部门。 + */ + void insertParentList(@Param("parentDeptId") Long parentDeptId, @Param("myDeptId") Long myDeptId); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java new file mode 100644 index 00000000..da04a33c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysMenuMapper.java @@ -0,0 +1,40 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysMenu; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 菜单数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysMenuMapper extends BaseDaoMapper { + + /** + * 获取登录用户的菜单列表。 + * + * @param userId 登录用户。 + * @return 菜单列表。 + */ + List getMenuListByUserId(@Param("userId") Long userId); + + /** + * 获取指定角色Id集合的菜单列表。 + * + * @param roleIds 角色Id集合。 + * @return 菜单列表。 + */ + List getMenuListByRoleIds(@Param("roleIds") Set roleIds); + + /** + * 查询包含指定菜单编码的菜单数量,目前仅用于satoken的权限框架。 + * + * @param menuCode 菜单编码。 + * @return 查询数量 + */ + int countMenuCode(@Param("menuCode") String menuCode); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java new file mode 100644 index 00000000..52a78fbf --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPermWhitelistMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysPermWhitelist; + +/** + * 权限资源白名单数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPermWhitelistMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java new file mode 100644 index 00000000..4d17cc24 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysPostMapper.java @@ -0,0 +1,52 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysPost; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 岗位管理数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPostMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param sysPostFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysPostList( + @Param("sysPostFilter") SysPost sysPostFilter, @Param("orderBy") String orderBy); + + /** + * 获取指定部门的岗位列表。 + * + * @param deptId 部门Id。 + * @param sysPostFilter 从表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 岗位数据列表。 + */ + List getSysPostListByDeptId( + @Param("deptId") Long deptId, + @Param("sysPostFilter") SysPost sysPostFilter, + @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表中没有和主表建立关联关系的数据列表。 + * + * @param deptId 关联主表Id。 + * @param sysPostFilter 过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 与主表没有建立关联的从表数据列表。 + */ + List getNotInSysPostListByDeptId( + @Param("deptId") Long deptId, + @Param("sysPostFilter") SysPost sysPostFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java new file mode 100644 index 00000000..9187244e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMapper.java @@ -0,0 +1,25 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysRole; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 角色数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysRoleMapper extends BaseDaoMapper { + + /** + * 获取对象列表,过滤条件中包含like和between条件。 + * + * @param sysRoleFilter 过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysRoleList(@Param("sysRoleFilter") SysRole sysRoleFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java new file mode 100644 index 00000000..38e63912 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysRoleMenuMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysRoleMenu; + +/** + * 角色与菜单操作关联关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysRoleMenuMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java new file mode 100644 index 00000000..055985d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserMapper.java @@ -0,0 +1,188 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysUser; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 用户管理数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserMapper extends BaseDaoMapper { + + /** + * 批量插入对象列表。 + * + * @param sysUserList 新增对象列表。 + */ + void insertList(List sysUserList); + + /** + * 获取过滤后的对象列表。 + * + * @param sysUserFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getSysUserList( + @Param("sysUserFilter") SysUser sysUserFilter, @Param("orderBy") String orderBy); + + /** + * 根据部门Id集合,获取关联的用户列表。 + * + * @param deptIds 关联的部门Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和部门Id集合关联的用户列表。 + */ + List getSysUserListByDeptIds( + @Param("deptIds") Set deptIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据登录名集合,获取关联的用户列表。 + * @param loginNames 登录名集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和登录名集合关联的用户列表。 + */ + List getSysUserListByLoginNames( + @Param("loginNames") List loginNames, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据角色Id,获取关联的用户列表。 + * + * @param roleId 关联的角色Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和角色Id关联的用户列表。 + */ + List getSysUserListByRoleId( + @Param("roleId") Long roleId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据角色Id集合,获取去重后的用户Id列表。 + * + * @param roleIds 关联的角色Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和角色Id集合关联的去重后的用户Id列表。 + */ + List getUserIdListByRoleIds( + @Param("roleIds") Set roleIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据角色Id,获取和当前角色Id没有建立多对多关联关系的用户列表。 + * + * @param roleId 关联的角色Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和RoleId没有建立关联关系的用户列表。 + */ + List getNotInSysUserListByRoleId( + @Param("roleId") Long roleId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据数据权限Id,获取关联的用户列表。 + * + * @param dataPermId 关联的数据权限Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和DataPermId关联的用户列表。 + */ + List getSysUserListByDataPermId( + @Param("dataPermId") Long dataPermId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据数据权限Id,获取和当前数据权限Id没有建立多对多关联关系的用户列表。 + * + * @param dataPermId 关联的数据权限Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和DataPermId没有建立关联关系的用户列表。 + */ + List getNotInSysUserListByDataPermId( + @Param("dataPermId") Long dataPermId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据部门岗位Id集合,获取关联的去重后的用户Id列表。 + * + * @param deptPostIds 关联的部门岗位Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和部门岗位Id集合关联的去重后的用户Id列表。 + */ + List getUserIdListByDeptPostIds( + @Param("deptPostIds") Set deptPostIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据部门岗位Id,获取关联的用户列表。 + * + * @param deptPostId 关联的部门岗位Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和部门岗位Id关联的用户列表。 + */ + List getSysUserListByDeptPostId( + @Param("deptPostId") Long deptPostId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据部门岗位Id,获取和当前部门岗位Id没有建立多对多关联关系的用户列表。 + * + * @param deptPostId 关联的部门岗位Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和deptPostId没有建立关联关系的用户列表。 + */ + List getNotInSysUserListByDeptPostId( + @Param("deptPostId") Long deptPostId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据岗位Id集合,获取关联的去重后的用户Id列表。 + * + * @param postIds 关联的岗位Id集合。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和岗位Id集合关联的去重后的用户Id列表。 + */ + List getUserIdListByPostIds( + @Param("postIds") Set postIds, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); + + /** + * 根据岗位Id,获取关联的用户列表。 + * + * @param postId 关联的岗位Id。 + * @param sysUserFilter 用户过滤条件对象。 + * @param orderBy order by从句的参数。 + * @return 和岗位Id关联的用户列表。 + */ + List getSysUserListByPostId( + @Param("postId") Long postId, + @Param("sysUserFilter") SysUser sysUserFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java new file mode 100644 index 00000000..6da64992 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserPostMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysUserPost; + +/** + * 用户岗位数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserPostMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java new file mode 100644 index 00000000..bf6dcfb8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/SysUserRoleMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.webadmin.upms.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.model.SysUserRole; + +/** + * 用户与角色关联关系数据访问操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserRoleMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml new file mode 100644 index 00000000..d3b228e6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml new file mode 100644 index 00000000..02c2e688 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMapper.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + AND zz_sys_data_perm.rule_type = #{sysDataPermFilter.ruleType} + + + + AND IFNULL(zz_sys_data_perm.data_perm_name, '') LIKE #{safeSearchString} + + + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml new file mode 100644 index 00000000..c668302f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml new file mode 100644 index 00000000..2530c39f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml new file mode 100644 index 00000000..ef63bdc9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + INSERT INTO zz_sys_dept + (dept_id, + dept_name, + show_order, + parent_id, + deleted_flag, + create_user_id, + update_user_id, + create_time, + update_time) + VALUES + + (#{item.deptId}, + #{item.deptName}, + #{item.showOrder}, + #{item.parentId}, + #{item.deletedFlag}, + #{item.createUserId}, + #{item.updateUserId}, + #{item.createTime}, + #{item.updateTime}) + + + + + + + + AND zz_sys_dept.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + + AND zz_sys_dept.dept_name LIKE #{safeSysDeptDeptName} + + + AND zz_sys_dept.parent_id = #{sysDeptFilter.parentId} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml new file mode 100644 index 00000000..5d03d88b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptPostMapper.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml new file mode 100644 index 00000000..37ebd397 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + DELETE a FROM zz_sys_dept_relation a + INNER JOIN zz_sys_dept_relation b ON a.dept_id = b.dept_id + WHERE b.parent_dept_id = #{myDeptId} AND a.parent_dept_id IN + + #{item} + + + + + INSERT INTO zz_sys_dept_relation(parent_dept_id, dept_id) VALUES + + (#{item.parentDeptId}, #{item.deptId}) + + + + + INSERT INTO zz_sys_dept_relation(parent_dept_id, dept_id) + SELECT t.parent_dept_id, #{myDeptId} FROM zz_sys_dept_relation t + WHERE t.dept_id = #{parentDeptId} + UNION ALL + SELECT #{myDeptId}, #{myDeptId} + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml new file mode 100644 index 00000000..d9ba9e7b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysMenuMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml new file mode 100644 index 00000000..00d0c6d4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml new file mode 100644 index 00000000..50765655 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysPostMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_sys_post.post_name LIKE #{safeSysPostPostName} + + + AND zz_sys_post.leader_post = #{sysPostFilter.leaderPost} + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml new file mode 100644 index 00000000..26b8e587 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + AND role_name LIKE #{safeRoleName} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml new file mode 100644 index 00000000..6bf30195 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml new file mode 100644 index 00000000..162d6b2d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserMapper.xml @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO zz_sys_user + (user_id, + login_name, + password, + dept_id, + show_name, + user_type, + head_image_url, + user_status, + email, + mobile, + create_user_id, + update_user_id, + create_time, + update_time, + deleted_flag) + VALUES + + (#{item.userId}, + #{item.loginName}, + #{item.password}, + #{item.deptId}, + #{item.showName}, + #{item.userType}, + #{item.headImageUrl}, + #{item.userStatus}, + #{item.email}, + #{item.mobile}, + #{item.createUserId}, + #{item.updateUserId}, + #{item.createTime}, + #{item.updateTime}, + #{item.deletedFlag}) + + + + + + + + AND zz_sys_user.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + + AND zz_sys_user.login_name LIKE #{safeSysUserLoginName} + + + AND (EXISTS (SELECT 1 FROM zz_sys_dept_relation WHERE + zz_sys_dept_relation.parent_dept_id = #{sysUserFilter.deptId} + AND zz_sys_user.dept_id = zz_sys_dept_relation.dept_id)) + + + + AND zz_sys_user.show_name LIKE #{safeSysUserShowName} + + + AND zz_sys_user.user_status = #{sysUserFilter.userStatus} + + + AND zz_sys_user.create_time >= #{sysUserFilter.createTimeStart} + + + AND zz_sys_user.create_time <= #{sysUserFilter.createTimeEnd} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml new file mode 100644 index 00000000..b846ba04 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserPostMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml new file mode 100644 index 00000000..c4993db0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dao/mapper/SysUserRoleMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java new file mode 100644 index 00000000..69aa2867 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDeptDto.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与部门关联Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与部门关联Dto") +@Data +public class SysDataPermDeptDto { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long dataPermId; + + /** + * 关联部门Id。 + */ + @Schema(description = "关联部门Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long deptId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java new file mode 100644 index 00000000..725c8068 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermDto.java @@ -0,0 +1,55 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.constant.DataPermRuleType; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 数据权限Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限Dto") +@Data +public class SysDataPermDto { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据权限Id不能为空!", groups = {UpdateGroup.class}) + private Long dataPermId; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据权限名称不能为空!") + private String dataPermName; + + /** + * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。 + */ + @Schema(description = "数据权限规则类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据权限规则类型不能为空!") + @ConstDictRef(constDictClass = DataPermRuleType.class) + private Integer ruleType; + + /** + * 部门Id列表(逗号分隔)。 + */ + @Schema(hidden = true) + private String deptIdListString; + + /** + * 搜索字符串。 + */ + @Schema(description = "LIKE 模糊搜索字符串") + private String searchString; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java new file mode 100644 index 00000000..763e9ddc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDataPermMenuDto.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与菜单关联Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与菜单关联Dto") +@Data +public class SysDataPermMenuDto { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long dataPermId; + + /** + * 关联菜单Id。 + */ + @Schema(description = "关联菜单Id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long menuId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java new file mode 100644 index 00000000..335f1607 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptDto.java @@ -0,0 +1,48 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 部门管理Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysDeptDto对象") +@Data +public class SysDeptDto { + + /** + * 部门Id。 + */ + @Schema(description = "部门Id。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,部门Id不能为空!", groups = {UpdateGroup.class}) + private Long deptId; + + /** + * 部门名称。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "部门名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,部门名称不能为空!") + private String deptName; + + /** + * 显示顺序。 + */ + @Schema(description = "显示顺序。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,显示顺序不能为空!") + private Integer showOrder; + + /** + * 父部门Id。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "父部门Id。可支持等于操作符的列表数据过滤。") + private Long parentId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java new file mode 100644 index 00000000..6362ebe8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysDeptPostDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 部门岗位Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "部门岗位Dto") +@Data +public class SysDeptPostDto { + + /** + * 部门岗位Id。 + */ + @Schema(description = "部门岗位Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,部门岗位Id不能为空!", groups = {UpdateGroup.class}) + private Long deptPostId; + + /** + * 部门Id。 + */ + @Schema(description = "部门Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,部门Id不能为空!", groups = {UpdateGroup.class}) + private Long deptId; + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,岗位Id不能为空!", groups = {UpdateGroup.class}) + private Long postId; + + /** + * 部门岗位显示名称。 + */ + @Schema(description = "部门岗位显示名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,部门岗位显示名称不能为空!") + private String postShowName; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java new file mode 100644 index 00000000..986f8dae --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysMenuDto.java @@ -0,0 +1,92 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 菜单Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "菜单Dto") +@Data +public class SysMenuDto { + + /** + * 菜单Id。 + */ + @Schema(description = "菜单Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "菜单Id不能为空!", groups = {UpdateGroup.class}) + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null + */ + @Schema(description = "父菜单Id") + private Long parentId; + + /** + * 菜单显示名称。 + */ + @Schema(description = "菜单显示名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "菜单显示名称不能为空!") + private String menuName; + + /** + * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + @Schema(description = "菜单类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "菜单类型不能为空!") + @ConstDictRef(constDictClass = SysMenuType.class, message = "数据验证失败,菜单类型为无效值!") + private Integer menuType; + + /** + * 前端表单路由名称,仅用于menu_type为1的菜单类型。 + */ + @Schema(description = "前端表单路由名称") + private String formRouterName; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + @Schema(description = "在线表单主键Id") + private Long onlineFormId; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + @Schema(description = "统计页面主键Id") + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + @Schema(description = "仅用于在线表单的流程Id") + private Long onlineFlowEntryId; + + /** + * 菜单显示顺序 (值越小,排序越靠前)。 + */ + @Schema(description = "菜单显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "菜单显示顺序不能为空!") + private Integer showOrder; + + /** + * 菜单图标。 + */ + @Schema(description = "菜单显示图标") + private String icon; + + /** + * 附加信息。 + */ + @Schema(description = "附加信息") + private String extraData; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java new file mode 100644 index 00000000..c9bef765 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysPostDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 岗位Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "岗位Dto") +@Data +public class SysPostDto { + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,岗位Id不能为空!", groups = {UpdateGroup.class}) + private Long postId; + + /** + * 岗位名称。 + */ + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,岗位名称不能为空!") + private String postName; + + /** + * 岗位层级,数值越小级别越高。 + */ + @Schema(description = "岗位层级", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,岗位层级不能为空!") + private Integer postLevel; + + /** + * 是否领导岗位。 + */ + @Schema(description = "是否领导岗位", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,领导岗位不能为空!", groups = {UpdateGroup.class}) + private Boolean leaderPost; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java new file mode 100644 index 00000000..3a567acd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysRoleDto.java @@ -0,0 +1,32 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 角色Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "角色Dto") +@Data +public class SysRoleDto { + + /** + * 角色Id。 + */ + @Schema(description = "角色Id", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "角色Id不能为空!", groups = {UpdateGroup.class}) + private Long roleId; + + /** + * 角色名称。 + */ + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "角色名称不能为空!") + private String roleName; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java new file mode 100644 index 00000000..4a993689 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/dto/SysUserDto.java @@ -0,0 +1,110 @@ +package com.orangeforms.webadmin.upms.dto; + +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.webadmin.upms.model.constant.SysUserType; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 用户管理Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysUserDto对象") +@Data +public class SysUserDto { + + /** + * 用户Id。 + */ + @Schema(description = "用户Id。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户Id不能为空!", groups = {UpdateGroup.class}) + private Long userId; + + /** + * 登录用户名。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "登录用户名。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,登录用户名不能为空!") + private String loginName; + + /** + * 用户密码。 + */ + @Schema(description = "用户密码。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,用户密码不能为空!", groups = {AddGroup.class}) + private String password; + + /** + * 用户部门Id。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "用户部门Id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户部门Id不能为空!") + private Long deptId; + + /** + * 用户显示名称。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "用户显示名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "数据验证失败,用户显示名称不能为空!") + private String showName; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)不能为空!") + @ConstDictRef(constDictClass = SysUserType.class, message = "数据验证失败,用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)为无效值!") + private Integer userType; + + /** + * 用户头像的Url。 + */ + @Schema(description = "用户头像的Url。") + private String headImageUrl; + + /** + * 用户状态(0: 正常 1: 锁定)。 + * NOTE: 可支持等于操作符的列表数据过滤。 + */ + @Schema(description = "用户状态(0: 正常 1: 锁定)。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "数据验证失败,用户状态(0: 正常 1: 锁定)不能为空!") + @ConstDictRef(constDictClass = SysUserStatus.class, message = "数据验证失败,用户状态(0: 正常 1: 锁定)为无效值!") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + @Schema(description = "用户邮箱。") + private String email; + + /** + * 用户手机。 + */ + @Schema(description = "用户手机。") + private String mobile; + + /** + * createTime 范围过滤起始值(>=)。 + * NOTE: 可支持范围操作符的列表数据过滤。 + */ + @Schema(description = "createTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。") + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + * NOTE: 可支持范围操作符的列表数据过滤。 + */ + @Schema(description = "createTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。") + private String createTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java new file mode 100644 index 00000000..c94e725d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPerm.java @@ -0,0 +1,62 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.annotation.RelationManyToMany; +import com.orangeforms.common.core.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.*; + +/** + * 数据权限实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "zz_sys_data_perm") +public class SysDataPerm extends BaseModel { + + /** + * 主键Id。 + */ + @TableId(value = "data_perm_id") + private Long dataPermId; + + /** + * 显示名称。 + */ + @TableField(value = "data_perm_name") + private String dataPermName; + + /** + * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。 + */ + @TableField(value = "rule_type") + private Integer ruleType; + + @TableField(exist = false) + private String deptIdListString; + + @RelationManyToMany( + relationMasterIdField = "dataPermId", + relationModelClass = SysDataPermDept.class) + @TableField(exist = false) + private List dataPermDeptList; + + @RelationManyToMany( + relationMasterIdField = "dataPermId", + relationModelClass = SysDataPermMenu.class) + @TableField(exist = false) + private List dataPermMenuList; + + @TableField(exist = false) + private String searchString; + + public void setSearchString(String searchString) { + this.searchString = MyCommonUtil.replaceSqlWildcard(searchString); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java new file mode 100644 index 00000000..89acecdb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermDept.java @@ -0,0 +1,29 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.ToString; + +/** + * 数据权限与部门关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString(of = {"deptId"}) +@TableName(value = "zz_sys_data_perm_dept") +public class SysDataPermDept { + + /** + * 数据权限Id。 + */ + @TableField(value = "data_perm_id") + private Long dataPermId; + + /** + * 关联部门Id。 + */ + @TableField(value = "dept_id") + private Long deptId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java new file mode 100644 index 00000000..2aad76fa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermMenu.java @@ -0,0 +1,29 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.ToString; + +/** + * 数据权限与菜单关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString(of = {"menuId"}) +@TableName(value = "zz_sys_data_perm_menu") +public class SysDataPermMenu { + + /** + * 数据权限Id。 + */ + @TableField(value = "data_perm_id") + private Long dataPermId; + + /** + * 关联菜单Id。 + */ + @TableField(value = "menu_id") + private Long menuId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java new file mode 100644 index 00000000..a30867b6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDataPermUser.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 数据权限与用户关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_data_perm_user") +public class SysDataPermUser { + + /** + * 数据权限Id。 + */ + @TableField(value = "data_perm_id") + private Long dataPermId; + + /** + * 用户Id。 + */ + @TableField(value = "user_id") + private Long userId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java new file mode 100644 index 00000000..3ce33929 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDept.java @@ -0,0 +1,72 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 部门管理实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_dept") +public class SysDept { + + /** + * 部门Id。 + */ + @TableId(value = "dept_id") + private Long deptId; + + /** + * 部门名称。 + */ + @TableField(value = "dept_name") + private String deptName; + + /** + * 显示顺序。 + */ + @TableField(value = "show_order") + private Integer showOrder; + + /** + * 父部门Id。 + */ + @TableField(value = "parent_id") + private Long parentId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java new file mode 100644 index 00000000..826f3724 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptPost.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 部门岗位多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_dept_post") +public class SysDeptPost { + + /** + * 部门岗位Id。 + */ + @TableId(value = "dept_post_id") + private Long deptPostId; + + /** + * 部门Id。 + */ + @TableField(value = "dept_id") + private Long deptId; + + /** + * 岗位Id。 + */ + @TableField(value = "post_id") + private Long postId; + + /** + * 部门岗位显示名称。 + */ + @TableField(value = "post_show_name") + private String postShowName; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java new file mode 100644 index 00000000..9b9c4146 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysDeptRelation.java @@ -0,0 +1,31 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 部门关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "zz_sys_dept_relation") +public class SysDeptRelation { + + /** + * 上级部门Id。 + */ + @TableField(value = "parent_dept_id") + private Long parentDeptId; + + /** + * 部门Id。 + */ + @TableField(value = "dept_id") + private Long deptId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java new file mode 100644 index 00000000..6f8a8a40 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysMenu.java @@ -0,0 +1,96 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.base.model.BaseModel; +import com.orangeforms.webadmin.upms.bo.SysMenuExtraData; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "zz_sys_menu") +public class SysMenu extends BaseModel { + + /** + * 菜单Id。 + */ + @TableId(value = "menu_id") + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null。 + */ + @TableField(value = "parent_id") + private Long parentId; + + /** + * 菜单显示名称。 + */ + @TableField(value = "menu_name") + private String menuName; + + /** + * 菜单类型(0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + @TableField(value = "menu_type") + private Integer menuType; + + /** + * 前端表单路由名称,仅用于menu_type为1的菜单类型。 + */ + @TableField(value = "form_router_name") + private String formRouterName; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + @TableField(value = "online_form_id") + private Long onlineFormId; + + /** + * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。 + */ + @TableField(value = "online_menu_perm_type") + private Integer onlineMenuPermType; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + @TableField(value = "report_page_id") + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + @TableField(value = "online_flow_entry_id") + private Long onlineFlowEntryId; + + /** + * 菜单显示顺序 (值越小,排序越靠前)。 + */ + @TableField(value = "show_order") + private Integer showOrder; + + /** + * 菜单图标。 + */ + private String icon; + + /** + * 附加信息。 + */ + @TableField(value = "extra_data") + private String extraData; + + /** + * extraData字段解析后的对象数据。 + */ + @TableField(exist = false) + private SysMenuExtraData extraObject; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java new file mode 100644 index 00000000..3551a831 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPermWhitelist.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 白名单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_perm_whitelist") +public class SysPermWhitelist { + + /** + * 权限资源的URL。 + */ + @TableId(value = "perm_url") + private String permUrl; + + /** + * 权限资源所属模块名字(通常是Controller的名字)。 + */ + @TableField(value = "module_name") + private String moduleName; + + /** + * 权限的名称。 + */ + @TableField(value = "perm_name") + private String permName; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java new file mode 100644 index 00000000..4a75033f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysPost.java @@ -0,0 +1,48 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 岗位实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "zz_sys_post") +public class SysPost extends BaseModel { + + /** + * 岗位Id。 + */ + @TableId(value = "post_id") + private Long postId; + + /** + * 岗位名称。 + */ + @TableField(value = "post_name") + private String postName; + + /** + * 岗位层级,数值越小级别越高。 + */ + @TableField(value = "post_level") + private Integer postLevel; + + /** + * 是否领导岗位。 + */ + @TableField(value = "leader_post") + private Boolean leaderPost; + + /** + * postId 的多对多关联表数据对象。 + */ + @TableField(exist = false) + private SysDeptPost sysDeptPost; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java new file mode 100644 index 00000000..a51dabbd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRole.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationManyToMany; +import com.orangeforms.common.core.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.*; + +/** + * 角色实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "zz_sys_role") +public class SysRole extends BaseModel { + + /** + * 角色Id。 + */ + @TableId(value = "role_id") + private Long roleId; + + /** + * 角色名称。 + */ + @TableField(value = "role_name") + private String roleName; + + @RelationManyToMany( + relationMasterIdField = "roleId", + relationModelClass = SysRoleMenu.class) + @TableField(exist = false) + private List sysRoleMenuList; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java new file mode 100644 index 00000000..35e065ed --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysRoleMenu.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 角色菜单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_role_menu") +public class SysRoleMenu { + + /** + * 角色Id。 + */ + @TableField(value = "role_id") + private Long roleId; + + /** + * 菜单Id。 + */ + @TableField(value = "menu_id") + private Long menuId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java new file mode 100644 index 00000000..372dee3a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUser.java @@ -0,0 +1,171 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.webadmin.upms.model.constant.SysUserType; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.core.annotation.*; +import lombok.Data; + +import java.util.Date; +import java.util.Map; +import java.util.List; + +/** + * 用户管理实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_user") +public class SysUser { + + /** + * 用户Id。 + */ + @TableId(value = "user_id") + private Long userId; + + /** + * 登录用户名。 + */ + @TableField(value = "login_name") + private String loginName; + + /** + * 用户密码。 + */ + private String password; + + /** + * 用户部门Id。 + */ + @TableField(value = "dept_id") + private Long deptId; + + /** + * 用户显示名称。 + */ + @TableField(value = "show_name") + private String showName; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @TableField(value = "user_type") + private Integer userType; + + /** + * 用户头像的Url。 + */ + @UploadFlagColumn(storeType = UploadStoreTypeEnum.LOCAL_SYSTEM) + @TableField(value = "head_image_url") + private String headImageUrl; + + /** + * 用户状态(0: 正常 1: 锁定)。 + */ + @TableField(value = "user_status") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + private String email; + + /** + * 用户手机。 + */ + private String mobile; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @TableField(exist = false) + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @TableField(exist = false) + private String createTimeEnd; + + /** + * 多对多用户部门岗位数据集合。 + */ + @RelationManyToMany( + relationMasterIdField = "userId", + relationModelClass = SysUserPost.class) + @TableField(exist = false) + private List sysUserPostList; + + /** + * 多对多用户角色数据集合。 + */ + @RelationManyToMany( + relationMasterIdField = "userId", + relationModelClass = SysUserRole.class) + @TableField(exist = false) + private List sysUserRoleList; + + /** + * 多对多用户数据权限数据集合。 + */ + @RelationManyToMany( + relationMasterIdField = "userId", + relationModelClass = SysDataPermUser.class) + @TableField(exist = false) + private List sysDataPermUserList; + + @RelationDict( + masterIdField = "deptId", + slaveModelClass = SysDept.class, + slaveIdField = "deptId", + slaveNameField = "deptName") + @TableField(exist = false) + private Map deptIdDictMap; + + @RelationConstDict( + masterIdField = "userType", + constantDictClass = SysUserType.class) + @TableField(exist = false) + private Map userTypeDictMap; + + @RelationConstDict( + masterIdField = "userStatus", + constantDictClass = SysUserStatus.class) + @TableField(exist = false) + private Map userStatusDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java new file mode 100644 index 00000000..b696f93e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserPost.java @@ -0,0 +1,33 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 用户岗位多对多关系实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_user_post") +public class SysUserPost { + + /** + * 用户Id。 + */ + @TableField(value = "user_id") + private Long userId; + + /** + * 部门岗位Id。 + */ + @TableField(value = "dept_post_id") + private Long deptPostId; + + /** + * 岗位Id。 + */ + @TableField(value = "post_id") + private Long postId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java new file mode 100644 index 00000000..62623c70 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/SysUserRole.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 用户角色实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_sys_user_role") +public class SysUserRole { + + /** + * 用户Id。 + */ + @TableField(value = "user_id") + private Long userId; + + /** + * 角色Id。 + */ + @TableField(value = "role_id") + private Long roleId; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java new file mode 100644 index 00000000..6108183d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysMenuType.java @@ -0,0 +1,54 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 菜单类型常量对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysMenuType { + + /** + * 目录菜单。 + */ + public static final int TYPE_DIRECTORY = 0; + /** + * 普通菜单。 + */ + public static final int TYPE_MENU = 1; + /** + * 表单片段类型。 + */ + public static final int TYPE_UI_FRAGMENT = 2; + /** + * 按钮类型。 + */ + public static final int TYPE_BUTTON = 3; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(TYPE_DIRECTORY, "目录菜单"); + DICT_MAP.put(TYPE_MENU, "普通菜单"); + DICT_MAP.put(TYPE_UI_FRAGMENT, "表单片段类型"); + DICT_MAP.put(TYPE_BUTTON, "按钮类型"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysMenuType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java new file mode 100644 index 00000000..752ce7dd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysOnlineMenuPermType.java @@ -0,0 +1,44 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 菜单关联在线表单的控制权限类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysOnlineMenuPermType { + + /** + * 查看。 + */ + public static final int TYPE_VIEW = 0; + /** + * 编辑。 + */ + public static final int TYPE_EDIT = 1; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(TYPE_VIEW, "查看"); + DICT_MAP.put(TYPE_EDIT, "编辑"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysOnlineMenuPermType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java new file mode 100644 index 00000000..b71dd0aa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserStatus.java @@ -0,0 +1,44 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 用户状态常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysUserStatus { + + /** + * 正常状态。 + */ + public static final int STATUS_NORMAL = 0; + /** + * 锁定状态。 + */ + public static final int STATUS_LOCKED = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(STATUS_NORMAL, "正常状态"); + DICT_MAP.put(STATUS_LOCKED, "锁定状态"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysUserStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java new file mode 100644 index 00000000..ee6fa852 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/model/constant/SysUserType.java @@ -0,0 +1,49 @@ +package com.orangeforms.webadmin.upms.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 用户类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysUserType { + + /** + * 管理员。 + */ + public static final int TYPE_ADMIN = 0; + /** + * 系统操作员。 + */ + public static final int TYPE_SYSTEM = 1; + /** + * 普通操作员。 + */ + public static final int TYPE_OPERATOR = 2; + + private static final Map DICT_MAP = new HashMap<>(3); + static { + DICT_MAP.put(TYPE_ADMIN, "管理员"); + DICT_MAP.put(TYPE_SYSTEM, "系统操作员"); + DICT_MAP.put(TYPE_OPERATOR, "普通操作员"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysUserType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java new file mode 100644 index 00000000..0dff4fa6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDataPermService.java @@ -0,0 +1,114 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.webadmin.upms.model.*; + +import java.util.*; + +/** + * 数据权限数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDataPermService extends IBaseService { + + /** + * 保存新增的数据权限对象。 + * + * @param dataPerm 新增的数据权限对象。 + * @param deptIdSet 关联的部门Id列表。 + * @param menuIdSet 关联的菜单Id列表。 + * @return 新增后的数据权限对象。 + */ + SysDataPerm saveNew(SysDataPerm dataPerm, Set deptIdSet, Set menuIdSet); + + /** + * 更新数据权限对象。 + * + * @param dataPerm 更新的数据权限对象。 + * @param originalDataPerm 原有的数据权限对象。 + * @param deptIdSet 关联的部门Id列表。 + * @param menuIdSet 关联的菜单Id列表。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysDataPerm dataPerm, SysDataPerm originalDataPerm, Set deptIdSet, Set menuIdSet); + + /** + * 删除指定数据权限。 + * + * @param dataPermId 数据权限主键Id。 + * @return 删除成功返回true,否则false。 + */ + boolean remove(Long dataPermId); + + /** + * 获取数据权限列表及其关联数据。 + * + * @param filter 数据权限过滤对象。 + * @param orderBy 排序参数。 + * @return 数据权限查询列表。 + */ + List getSysDataPermListWithRelation(SysDataPerm filter, String orderBy); + + /** + * 将指定用户的指定会话的数据权限集合存入缓存。 + * + * @param sessionId 会话Id。 + * @param userId 用户主键Id。 + * @param deptId 用户所属部门主键Id。 + */ + void putDataPermCache(String sessionId, Long userId, Long deptId); + + /** + * 将指定会话的数据权限集合从缓存中移除。 + * + * @param sessionId 会话Id。 + */ + void removeDataPermCache(String sessionId); + + /** + * 获取指定用户Id的数据权限列表。并基于menuId和权限规则类型进行了一级分组。 + * + * @param userId 指定的用户Id。 + * @param deptId 用户所属部门主键Id。 + * @return 合并优化后的数据权限列表。返回格式为,Map>。 + */ + Map> getSysDataPermListByUserId(Long userId, Long deptId); + + /** + * 查询与指定菜单关联的数据权限列表。 + * + * @param menuId 菜单Id。 + * @return 与菜单Id关联的数据权限列表。 + */ + List getSysDataPermListByMenuId(Long menuId); + + /** + * 添加用户和数据权限之间的多对多关联关系。 + * + * @param dataPermId 数据权限Id。 + * @param userIdSet 关联的用户Id列表。 + */ + void addDataPermUserList(Long dataPermId, Set userIdSet); + + /** + * 移除用户和数据权限之间的多对多关联关系。 + * + * @param dataPermId 数据权限主键Id。 + * @param userId 用户主键Id。 + * @return true移除成功,否则false。 + */ + boolean removeDataPermUser(Long dataPermId, Long userId); + + /** + * 验证数据权限对象关联菜单数据是否都合法。 + * + * @param dataPerm 数据权限关对象。 + * @param deptIdListString 与数据权限关联的部门Id列表。 + * @param menuIdListString 与数据权限关联的菜单Id列表。 + * @return 验证结果。 + */ + CallResult verifyRelatedData(SysDataPerm dataPerm, String deptIdListString, String menuIdListString); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java new file mode 100644 index 00000000..2a485df5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysDeptService.java @@ -0,0 +1,170 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 部门管理数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysDeptService extends IBaseService { + + /** + * 保存新增的部门对象。 + * + * @param sysDept 新增的部门对象。 + * @param parentSysDept 上级部门对象。 + * @return 新增后的部门对象。 + */ + SysDept saveNew(SysDept sysDept, SysDept parentSysDept); + + /** + * 更新部门对象。 + * + * @param sysDept 更新的部门对象。 + * @param originalSysDept 原有的部门对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysDept sysDept, SysDept originalSysDept); + + /** + * 删除指定数据。 + * + * @param deptId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long deptId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getSysDeptListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysDeptList(SysDept filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getSysDeptList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysDeptListWithRelation(SysDept filter, String orderBy); + + /** + * 判断指定对象是否包含下级对象。 + * + * @param deptId 主键Id。 + * @return 存在返回true,否则false。 + */ + boolean hasChildren(Long deptId); + + /** + * 判断指定部门Id是否包含用户对象。 + * + * @param deptId 部门主键Id。 + * @return 存在返回true,否则false。 + */ + boolean hasChildrenUser(Long deptId); + + /** + * 批量添加多对多关联关系。 + * + * @param sysDeptPostList 多对多关联表对象集合。 + * @param deptId 主表Id。 + */ + void addSysDeptPostList(List sysDeptPostList, Long deptId); + + /** + * 更新中间表数据。 + * + * @param sysDeptPost 中间表对象。 + * @return 更新成功与否。 + */ + boolean updateSysDeptPost(SysDeptPost sysDeptPost); + + /** + * 移除单条多对多关系。 + * + * @param deptId 主表Id。 + * @param postId 从表Id。 + * @return 成功返回true,否则false。 + */ + boolean removeSysDeptPost(Long deptId, Long postId); + + /** + * 获取中间表数据。 + * + * @param deptId 主表Id。 + * @param postId 从表Id。 + * @return 中间表对象。 + */ + SysDeptPost getSysDeptPost(Long deptId, Long postId); + + /** + * 根据部门岗位Id获取部门岗位关联对象。 + * + * @param deptPostId 部门岗位Id。 + * @return 部门岗位对象。 + */ + SysDeptPost getSysDeptPost(Long deptPostId); + + /** + * 获取指定部门Id的部门岗位多对多关联数据列表,以及关联的部门和岗位数据。 + * + * @param deptId 部门Id。如果参数为空则返回全部数据。 + * @return 部门岗位多对多数据列表。 + */ + List> getSysDeptPostListWithRelationByDeptId(Long deptId); + + /** + * 获取指定部门Id和岗位Id集合的部门岗位多对多关联数据列表。 + * + * @param deptId 部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 部门岗位多对多数据列表。 + */ + List getSysDeptPostList(Long deptId, Set postIdSet); + + /** + * 获取与指定部门Id同级部门和岗位Id集合的部门岗位多对多关联数据列表。 + * + * @param deptId 部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 部门岗位多对多数据列表。 + */ + List getSiblingSysDeptPostList(Long deptId, Set postIdSet); + + /** + * 根据部门Id获取该部门领导岗位的部门岗位Id集合。 + * + * @param deptId 部门Id。 + * @return 部门领导岗位的部门岗位Id集合。 + */ + List getLeaderDeptPostIdList(Long deptId); + + /** + * 根据部门Id获取上级部门领导岗位的部门岗位Id集合。 + * + * @param deptId 部门Id。 + * @return 上级部门领导岗位的部门岗位Id集合。 + */ + List getUpLeaderDeptPostIdList(Long deptId); + + /** + * 根据父主键Id列表,获取当前部门Id及其所有下级部门Id列表。 + * + * @param parentIds 父主键Id列表。 + * @return 获取当前部门Id及其所有下级部门Id列表。 + */ + List getAllChildDeptIdByParentIds(List parentIds); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java new file mode 100644 index 00000000..7c39d7e8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysMenuService.java @@ -0,0 +1,72 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.webadmin.upms.model.SysMenu; + +import java.util.*; + +/** + * 菜单数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysMenuService extends IBaseService { + + /** + * 保存新增的菜单对象。 + * + * @param sysMenu 新增的菜单对象。 + * @return 新增后的菜单对象。 + */ + SysMenu saveNew(SysMenu sysMenu); + + /** + * 更新菜单对象。 + * + * @param sysMenu 更新的菜单对象。 + * @param originalSysMenu 原有的菜单对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysMenu sysMenu, SysMenu originalSysMenu); + + /** + * 删除指定的菜单。 + * + * @param menu 菜单对象。 + * @return 删除成功返回true,否则false。 + */ + boolean remove(SysMenu menu); + + /** + * 获取指定用户Id的菜单列表,已去重。 + * + * @param userId 用户主键Id。 + * @return 用户关联的菜单列表。 + */ + Collection getMenuListByUserId(Long userId); + + /** + * 根据角色Id集合获取菜单对象列表。 + * + * @param roleIds 逗号分隔的角色Id集合。 + * @return 菜单对象列表。 + */ + Collection getMenuListByRoleIds(String roleIds); + + /** + * 判断当前菜单是否存在子菜单。 + * + * @param menuId 菜单主键Id。 + * @return 存在返回true,否则false。 + */ + boolean hasChildren(Long menuId); + + /** + * 获取指定类型的所有在线表单的菜单。 + * + * @param menuType 菜单类型,NULL则返回全部类型。 + * @return 在线表单关联的菜单列表。 + */ + List getAllOnlineMenuList(Integer menuType); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java new file mode 100644 index 00000000..84dab9fa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPermWhitelistService.java @@ -0,0 +1,23 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.webadmin.upms.model.SysPermWhitelist; + +import java.util.List; + +/** + * 权限资源白名单数据服务接口。 + * 白名单中的权限资源,可以不受权限控制,任何用户皆可访问,一般用于常用的字典数据列表接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPermWhitelistService extends IBaseService { + + /** + * 获取白名单权限资源的列表。 + * + * @return 白名单权限资源地址列表。 + */ + List getWhitelistPermList(); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java new file mode 100644 index 00000000..71165759 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysPostService.java @@ -0,0 +1,99 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.webadmin.upms.model.SysPost; +import com.orangeforms.webadmin.upms.model.SysUserPost; + +import java.util.*; + +/** + * 岗位管理数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysPostService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param sysPost 新增对象。 + * @return 返回新增对象。 + */ + SysPost saveNew(SysPost sysPost); + + /** + * 更新数据对象。 + * + * @param sysPost 更新的对象。 + * @param originalSysPost 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(SysPost sysPost, SysPost originalSysPost); + + /** + * 删除指定数据。 + * + * @param postId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long postId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getSysPostListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysPostList(SysPost filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getSysPostList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysPostListWithRelation(SysPost filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。 + * + * @param deptId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getNotInSysPostListByDeptId(Long deptId, SysPost filter, String orderBy); + + /** + * 获取指定部门的岗位列表。 + * + * @param deptId 部门Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysPostListByDeptId(Long deptId, SysPost filter, String orderBy); + + /** + * 获取指定用户的用户岗位多对多关联数据列表。 + * + * @param userId 用户Id。 + * @return 用户岗位多对多关联数据列表。 + */ + List getSysUserPostListByUserId(Long userId); + + /** + * 判断指定的部门岗位Id集合是否都属于指定的部门Id。 + * + * @param deptPostIdSet 部门岗位Id集合。 + * @param deptId 部门Id。 + * @return 全部是返回true,否则false。 + */ + boolean existAllPrimaryKeys(Set deptPostIdSet, Long deptId); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java new file mode 100644 index 00000000..1f6762d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysRoleService.java @@ -0,0 +1,87 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.webadmin.upms.model.SysRole; +import com.orangeforms.webadmin.upms.model.SysUserRole; + +import java.util.*; + +/** + * 角色数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysRoleService extends IBaseService { + + /** + * 保存新增的角色对象。 + * + * @param role 新增的角色对象。 + * @param menuIdSet 菜单Id列表。 + * @return 新增后的角色对象。 + */ + SysRole saveNew(SysRole role, Set menuIdSet); + + /** + * 更新角色对象。 + * + * @param role 更新的角色对象。 + * @param originalRole 原有的角色对象。 + * @param menuIdSet 菜单Id列表。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysRole role, SysRole originalRole, Set menuIdSet); + + /** + * 删除指定角色。 + * + * @param roleId 角色主键Id。 + * @return 删除成功返回true,否则false。 + */ + boolean remove(Long roleId); + + /** + * 获取角色列表。 + * + * @param filter 角色过滤对象。 + * @param orderBy 排序参数。 + * @return 角色列表。 + */ + List getSysRoleList(SysRole filter, String orderBy); + + /** + * 获取用户的用户角色对象列表。 + * + * @param userId 用户Id。 + * @return 用户角色对象列表。 + */ + List getSysUserRoleListByUserId(Long userId); + + /** + * 批量新增用户角色关联。 + * + * @param userRoleList 用户角色关系数据列表。 + */ + void addUserRoleList(List userRoleList); + + /** + * 移除指定用户和指定角色的关联关系。 + * + * @param roleId 角色主键Id。 + * @param userId 用户主键Id。 + * @return 移除成功返回true,否则false。 + */ + boolean removeUserRole(Long roleId, Long userId); + + /** + * 验证角色对象关联的数据是否都合法。 + * + * @param sysRole 当前操作的对象。 + * @param originalSysRole 原有对象。 + * @param menuIdListString 逗号分隔的menuId列表。 + * @return 验证结果。 + */ + CallResult verifyRelatedData(SysRole sysRole, SysRole originalSysRole, String menuIdListString); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java new file mode 100644 index 00000000..15fa2ea2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/SysUserService.java @@ -0,0 +1,176 @@ +package com.orangeforms.webadmin.upms.service; + +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 用户管理数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysUserService extends IBaseService { + + /** + * 获取指定登录名的用户对象。 + * + * @param loginName 指定登录用户名。 + * @return 用户对象。 + */ + SysUser getSysUserByLoginName(String loginName); + + /** + * 保存新增的用户对象。 + * + * @param user 新增的用户对象。 + * @param roleIdSet 用户角色Id集合。 + * @param deptPostIdSet 部门岗位Id集合。 + * @param dataPermIdSet 数据权限Id集合。 + * @return 新增后的用户对象。 + */ + SysUser saveNew(SysUser user, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet); + + /** + * 更新用户对象。 + * + * @param user 更新的用户对象。 + * @param originalUser 原有的用户对象。 + * @param roleIdSet 用户角色Id列表。 + * @param deptPostIdSet 部门岗位Id集合。 + * @param dataPermIdSet 数据权限Id集合。 + * @return 更新成功返回true,否则false。 + */ + boolean update(SysUser user, SysUser originalUser, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet); + + /** + * 修改用户密码。 + * @param userId 用户主键Id。 + * @param newPass 新密码。 + * @return 成功返回true,否则false。 + */ + boolean changePassword(Long userId, String newPass); + + /** + * 修改用户头像。 + * + * @param userId 用户主键Id。 + * @param newHeadImage 新的头像信息。 + * @return 成功返回true,否则false。 + */ + boolean changeHeadImage(Long userId, String newHeadImage); + + /** + * 删除指定数据。 + * + * @param userId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long userId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getSysUserListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysUserList(SysUser filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getSysUserList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getSysUserListWithRelation(SysUser filter, String orderBy); + + /** + * 获取指定角色的用户列表。 + * + * @param roleId 角色主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByRoleId(Long roleId, SysUser filter, String orderBy); + + /** + * 获取不属于指定角色的用户列表。 + * + * @param roleId 角色主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getNotInSysUserListByRoleId(Long roleId, SysUser filter, String orderBy); + + /** + * 获取指定数据权限的用户列表。 + * + * @param dataPermId 数据权限主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy); + + /** + * 获取不属于指定数据权限的用户列表。 + * + * @param dataPermId 数据权限主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getNotInSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy); + + /** + * 获取指定部门岗位的用户列表。 + * + * @param deptPostId 部门岗位主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy); + + /** + * 获取不属于指定部门岗位的用户列表。 + * + * @param deptPostId 部门岗位主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getNotInSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy); + + /** + * 获取指定岗位的用户列表。 + * + * @param postId 岗位主键Id。 + * @param filter 用户过滤对象。 + * @param orderBy 排序参数。 + * @return 用户列表。 + */ + List getSysUserListByPostId(Long postId, SysUser filter, String orderBy); + + /** + * 验证用户对象关联的数据是否都合法。 + * + * @param sysUser 当前操作的对象。 + * @param originalSysUser 原有对象。 + * @param roleIds 逗号分隔的角色Id列表字符串。 + * @param deptPostIds 逗号分隔的部门岗位Id列表字符串。 + * @param dataPermIds 逗号分隔的数据权限Id列表字符串。 + * @return 验证结果。 + */ + CallResult verifyRelatedData( + SysUser sysUser, SysUser originalSysUser, String roleIds, String deptPostIds, String dataPermIds); +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java new file mode 100644 index 00000000..6e4a4e61 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDataPermServiceImpl.java @@ -0,0 +1,345 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.core.constant.DataPermRuleType; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.webadmin.config.ApplicationConfig; +import com.orangeforms.webadmin.upms.dao.SysDataPermDeptMapper; +import com.orangeforms.webadmin.upms.dao.SysDataPermMapper; +import com.orangeforms.webadmin.upms.dao.SysDataPermUserMapper; +import com.orangeforms.webadmin.upms.dao.SysDataPermMenuMapper; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.service.SysDataPermService; +import com.orangeforms.webadmin.upms.service.SysDeptService; +import com.orangeforms.webadmin.upms.service.SysMenuService; +import com.orangeforms.webadmin.upms.service.SysUserService; +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.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 数据权限数据服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysDataPermService") +public class SysDataPermServiceImpl extends BaseService implements SysDataPermService { + + @Autowired + private SysDataPermMapper sysDataPermMapper; + @Autowired + private SysDataPermDeptMapper sysDataPermDeptMapper; + @Autowired + private SysDataPermUserMapper sysDataPermUserMapper; + @Autowired + private SysDataPermMenuMapper sysDataPermMenuMapper; + @Autowired + private SysUserService sysUserService; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysMenuService sysMenuService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private ApplicationConfig applicationConfig; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysDataPermMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysDataPerm saveNew(SysDataPerm dataPerm, Set deptIdSet, Set menuIdSet) { + dataPerm.setDataPermId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(dataPerm); + sysDataPermMapper.insert(dataPerm); + this.insertRelationData(dataPerm, deptIdSet, menuIdSet); + return dataPerm; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update( + SysDataPerm dataPerm, SysDataPerm originalDataPerm, Set deptIdSet, Set menuIdSet) { + MyModelUtil.fillCommonsForUpdate(dataPerm, originalDataPerm); + UpdateWrapper uw = this.createUpdateQueryForNullValue(dataPerm, dataPerm.getDataPermId()); + if (sysDataPermMapper.update(dataPerm, uw) != 1) { + return false; + } + SysDataPermDept dataPermDept = new SysDataPermDept(); + dataPermDept.setDataPermId(dataPerm.getDataPermId()); + sysDataPermDeptMapper.delete(new QueryWrapper<>(dataPermDept)); + SysDataPermMenu dataPermMenu = new SysDataPermMenu(); + dataPermMenu.setDataPermId(dataPerm.getDataPermId()); + sysDataPermMenuMapper.delete(new QueryWrapper<>(dataPermMenu)); + this.insertRelationData(dataPerm, deptIdSet, menuIdSet); + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dataPermId) { + if (sysDataPermMapper.deleteById(dataPermId) != 1) { + return false; + } + SysDataPermDept dataPermDept = new SysDataPermDept(); + dataPermDept.setDataPermId(dataPermId); + sysDataPermDeptMapper.delete(new QueryWrapper<>(dataPermDept)); + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + sysDataPermUserMapper.delete(new QueryWrapper<>(dataPermUser)); + SysDataPermMenu dataPermMenu = new SysDataPermMenu(); + dataPermMenu.setDataPermId(dataPermId); + sysDataPermMenuMapper.delete(new QueryWrapper<>(dataPermMenu)); + return true; + } + + @Override + public List getSysDataPermListWithRelation(SysDataPerm filter, String orderBy) { + List resultList = sysDataPermMapper.getSysDataPermList(filter, orderBy); + buildRelationForDataList(resultList, MyRelationParam.full(), CollUtil.newHashSet("dataPermDeptList")); + return resultList; + } + + @Override + public void putDataPermCache(String sessionId, Long userId, Long deptId) { + Map> menuDataPermMap = getSysDataPermListByUserId(userId, deptId); + if (menuDataPermMap.size() > 0) { + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(sessionId); + RBucket bucket = redissonClient.getBucket(dataPermSessionKey); + bucket.set(JSON.toJSONString(menuDataPermMap), + applicationConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS); + } + } + + @Override + public void removeDataPermCache(String sessionId) { + String sessionPermKey = RedisKeyUtil.makeSessionDataPermIdKey(sessionId); + redissonClient.getBucket(sessionPermKey).deleteAsync(); + } + + @Override + public Map> getSysDataPermListByUserId(Long userId, Long deptId) { + List dataPermList = sysDataPermMapper.getSysDataPermListByUserId(userId); + dataPermList.forEach(dataPerm -> { + if (CollUtil.isNotEmpty(dataPerm.getDataPermDeptList())) { + Set deptIdSet = dataPerm.getDataPermDeptList().stream() + .map(SysDataPermDept::getDeptId).collect(Collectors.toSet()); + dataPerm.setDeptIdListString(StrUtil.join(",", deptIdSet)); + } + }); + Map> menuIdMap = new HashMap<>(4); + for (SysDataPerm dataPerm : dataPermList) { + if (CollUtil.isNotEmpty(dataPerm.getDataPermMenuList())) { + for (SysDataPermMenu dataPermMenu : dataPerm.getDataPermMenuList()) { + menuIdMap.computeIfAbsent( + dataPermMenu.getMenuId().toString(), k -> new LinkedList<>()).add(dataPerm); + } + } else { + menuIdMap.computeIfAbsent( + ApplicationConstant.DATA_PERM_ALL_MENU_ID, k -> new LinkedList<>()).add(dataPerm); + } + } + Map> menuResultMap = new HashMap<>(menuIdMap.size()); + for (Map.Entry> entry : menuIdMap.entrySet()) { + Map resultMap = this.mergeAndOptimizeDataPermRule(entry.getValue(), deptId); + menuResultMap.put(entry.getKey(), resultMap); + } + return menuResultMap; + } + + @Override + public List getSysDataPermListByMenuId(Long menuId) { + return sysDataPermMapper.getSysDataPermListByMenuId(menuId); + } + + private Map mergeAndOptimizeDataPermRule(List dataPermList, Long deptId) { + // 为了更方便进行后续的合并优化处理,这里再基于菜单Id和规则类型进行分组。ruleMap的key是规则类型。 + Map> ruleMap = + dataPermList.stream().collect(Collectors.groupingBy(SysDataPerm::getRuleType)); + Map resultMap = new HashMap<>(ruleMap.size()); + // 如有有ALL存在,就可以直接退出了,没有必要在处理后续的规则了。 + if (ruleMap.containsKey(DataPermRuleType.TYPE_ALL)) { + resultMap.put(DataPermRuleType.TYPE_ALL, "null"); + return resultMap; + } + // 这里优先合并最复杂的多部门及子部门场景。 + String deptIds = processMultiDeptAndChildren(ruleMap, deptId); + if (deptIds != null) { + resultMap.put(DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT, deptIds); + } + // 合并当前部门及子部门的优化 + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) != null) { + // 需要与仅仅当前部门规则进行合并。 + ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY); + resultMap.put(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT, "null"); + } + // 合并自定义部门了。 + deptIds = processMultiDept(ruleMap, deptId); + if (deptIds != null) { + resultMap.put(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, deptIds); + } + // 最后处理当前部门和当前用户。 + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_ONLY) != null) { + resultMap.put(DataPermRuleType.TYPE_DEPT_ONLY, "null"); + } + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS) != null) { + // 合并当前部门用户和当前用户 + ruleMap.remove(DataPermRuleType.TYPE_USER_ONLY); + ruleMap.remove(DataPermRuleType.TYPE_DEPT_USERS); + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + List userList = sysUserService.getSysUserList(filter, null); + Set userIdSet = userList.stream().map(SysUser::getUserId).collect(Collectors.toSet()); + resultMap.put(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS, CollUtil.join(userIdSet, ",")); + } + if (ruleMap.get(DataPermRuleType.TYPE_DEPT_USERS) != null) { + SysUser filter = new SysUser(); + filter.setDeptId(deptId); + List userList = sysUserService.getListByFilter(filter); + Set userIdSet = userList.stream().map(SysUser::getUserId).collect(Collectors.toSet()); + // 合并仅当前用户 + ruleMap.remove(DataPermRuleType.TYPE_USER_ONLY); + resultMap.put(DataPermRuleType.TYPE_DEPT_USERS, CollUtil.join(userIdSet, ",")); + } + if (ruleMap.get(DataPermRuleType.TYPE_USER_ONLY) != null) { + resultMap.put(DataPermRuleType.TYPE_USER_ONLY, "null"); + } + return resultMap; + } + + private String processMultiDeptAndChildren(Map> ruleMap, Long deptId) { + List parentDeptList = ruleMap.get(DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT); + if (parentDeptList == null) { + return null; + } + Set deptIdSet = new HashSet<>(); + for (SysDataPerm parentDept : parentDeptList) { + deptIdSet.addAll(StrUtil.split(parentDept.getDeptIdListString(), ',') + .stream().map(Long::valueOf).collect(Collectors.toSet())); + } + // 在合并所有的多父部门Id之后,需要判断是否有本部门及子部门的规则。如果有,就继续合并。 + if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) { + // 如果多父部门列表中包含当前部门,那么可以直接删除该规则了,如果没包含,就加入到多部门的DEPT_ID的IN LIST中。 + deptIdSet.add(deptId); + ruleMap.remove(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT); + } + // 需要与仅仅当前部门规则进行合并。 + if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_ONLY) && deptIdSet.contains(deptId)) { + ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY); + } + return StrUtil.join(",", deptIdSet); + } + + private String processMultiDept(Map> ruleMap, Long deptId) { + List customDeptList = ruleMap.get(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST); + if (customDeptList == null) { + return null; + } + Set deptIdSet = new HashSet<>(); + for (SysDataPerm customDept : customDeptList) { + deptIdSet.addAll(StrUtil.split(customDept.getDeptIdListString(), ',') + .stream().map(Long::valueOf).collect(Collectors.toSet())); + } + if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_ONLY)) { + deptIdSet.add(deptId); + ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY); + } + return StrUtil.join(",", deptIdSet); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addDataPermUserList(Long dataPermId, Set userIdSet) { + for (Long userId : userIdSet) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(userId); + sysDataPermUserMapper.insert(dataPermUser); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeDataPermUser(Long dataPermId, Long userId) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(userId); + return sysDataPermUserMapper.delete(new QueryWrapper<>(dataPermUser)) == 1; + } + + @Override + public CallResult verifyRelatedData(SysDataPerm dataPerm, String deptIdListString, String menuIdListString) { + JSONObject jsonObject = new JSONObject(); + if (dataPerm.getRuleType() == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT + || dataPerm.getRuleType() == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) { + if (StrUtil.isBlank(deptIdListString)) { + return CallResult.error("数据验证失败,部门列表不能为空!"); + } + Set deptIdSet = StrUtil.split( + deptIdListString, ",").stream().map(Long::valueOf).collect(Collectors.toSet()); + if (!sysDeptService.existAllPrimaryKeys(deptIdSet)) { + return CallResult.error("数据验证失败,存在不合法的部门数据,请刷新后重试!"); + } + jsonObject.put("deptIdSet", deptIdSet); + } + if (StrUtil.isNotBlank(menuIdListString)) { + Set menuIdSet = StrUtil.split( + menuIdListString, ",").stream().map(Long::valueOf).collect(Collectors.toSet()); + if (!sysMenuService.existAllPrimaryKeys(menuIdSet)) { + return CallResult.error("数据验证失败,存在不合法的菜单数据,请刷新后重试!"); + } + jsonObject.put("menuIdSet", menuIdSet); + } + return CallResult.ok(jsonObject); + } + + private void insertRelationData(SysDataPerm dataPerm, Set deptIdSet, Set menuIdSet) { + if (CollUtil.isNotEmpty(deptIdSet)) { + for (Long deptId : deptIdSet) { + SysDataPermDept dataPermDept = new SysDataPermDept(); + dataPermDept.setDataPermId(dataPerm.getDataPermId()); + dataPermDept.setDeptId(deptId); + sysDataPermDeptMapper.insert(dataPermDept); + } + } + if (CollUtil.isNotEmpty(menuIdSet)) { + for (Long menuId : menuIdSet) { + SysDataPermMenu dataPermMenu = new SysDataPermMenu(); + dataPermMenu.setDataPermId(dataPerm.getDataPermId()); + dataPermMenu.setMenuId(menuId); + sysDataPermMenuMapper.insert(dataPermMenu); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java new file mode 100644 index 00000000..a23bd997 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,316 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.*; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.webadmin.upms.dao.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 部门管理数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysDeptService") +public class SysDeptServiceImpl extends BaseService implements SysDeptService, BizWidgetDatasource { + + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private SysDeptMapper sysDeptMapper; + @Autowired + private SysDeptRelationMapper sysDeptRelationMapper; + @Autowired + private SysUserService sysUserService; + @Autowired + private SysDeptPostMapper sysDeptPostMapper; + @Autowired + private SysDataPermDeptMapper sysDataPermDeptMapper; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysDeptMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_DEPT_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysDept.class); + SysDept deptFilter = filter == null ? null : BeanUtil.toBean(filter, SysDept.class); + List deptList = this.getSysDeptList(deptFilter, orderBy); + this.buildRelationForDataList(deptList, MyRelationParam.dictOnly()); + return MyPageUtil.makeResponseData(deptList, BeanUtil::beanToMap); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List deptList; + if (StrUtil.isBlank(fieldName)) { + deptList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + deptList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysDept.class, fieldName, fieldValues)); + } + this.buildRelationForDataList(deptList, MyRelationParam.dictOnly()); + return MyModelUtil.beanToMapList(deptList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysDept saveNew(SysDept sysDept, SysDept parentSysDept) { + sysDept.setDeptId(idGenerator.nextLongId()); + sysDept.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.fillCommonsForInsert(sysDept); + sysDeptMapper.insert(sysDept); + // 同步插入部门关联关系数据 + if (parentSysDept == null) { + sysDeptRelationMapper.insert(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId())); + } else { + sysDeptRelationMapper.insertParentList(parentSysDept.getDeptId(), sysDept.getDeptId()); + } + return sysDept; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysDept sysDept, SysDept originalSysDept) { + MyModelUtil.fillCommonsForUpdate(sysDept, originalSysDept); + UpdateWrapper uw = this.createUpdateQueryForNullValue(sysDept, sysDept.getDeptId()); + if (sysDeptMapper.update(sysDept, uw) == 0) { + return false; + } + if (ObjectUtil.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) { + this.updateParentRelation(sysDept, originalSysDept); + } + return true; + } + + private void updateParentRelation(SysDept sysDept, SysDept originalSysDept) { + List originalParentIdList = null; + // 1. 因为层级关系变化了,所以要先遍历出,当前部门的原有父部门Id列表。 + if (originalSysDept.getParentId() != null) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getDeptId()); + List relationList = sysDeptRelationMapper.selectList(queryWrapper); + originalParentIdList = relationList.stream() + .filter(c -> !c.getParentDeptId().equals(sysDept.getDeptId())) + .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList()); + } + // 2. 毕竟当前部门的上级部门变化了,所以当前部门和他的所有子部门,与当前部门的原有所有上级部门 + // 之间的关联关系就要被移除。 + // 这里先移除当前部门的所有子部门,与当前部门的所有原有上级部门之间的关联关系。 + if (CollUtil.isNotEmpty(originalParentIdList)) { + sysDeptRelationMapper.removeBetweenChildrenAndParents(originalParentIdList, sysDept.getDeptId()); + } + // 这里更进一步,将当前部门Id与其原有所有上级部门Id之间的关联关系删除。 + SysDeptRelation filter = new SysDeptRelation(); + filter.setDeptId(sysDept.getDeptId()); + sysDeptRelationMapper.delete(new QueryWrapper<>(filter)); + // 3. 重新计算当前部门的新上级部门列表。 + List newParentIdList = new LinkedList<>(); + // 这里要重新计算出当前部门所有新的上级部门Id列表。 + if (sysDept.getParentId() != null) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getParentId()); + List relationList = sysDeptRelationMapper.selectList(queryWrapper); + newParentIdList = relationList.stream() + .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList()); + } + // 4. 先查询出当前部门的所有下级子部门Id列表。 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDeptRelation::getParentDeptId, sysDept.getDeptId()); + List childRelationList = sysDeptRelationMapper.selectList(queryWrapper); + // 5. 将当前部门及其所有子部门Id与其新的所有上级部门Id之间,建立关联关系。 + List deptRelationList = new LinkedList<>(); + deptRelationList.add(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId())); + for (Long newParentId : newParentIdList) { + deptRelationList.add(new SysDeptRelation(newParentId, sysDept.getDeptId())); + for (SysDeptRelation childDeptRelation : childRelationList) { + deptRelationList.add(new SysDeptRelation(newParentId, childDeptRelation.getDeptId())); + } + } + // 6. 执行批量插入SQL语句,插入当前部门Id及其所有下级子部门Id,与所有新上级部门Id之间的关联关系。 + sysDeptRelationMapper.insertList(deptRelationList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long deptId) { + if (sysDeptMapper.deleteById(deptId) == 0) { + return false; + } + // 这里删除当前部门及其父部门的关联关系。 + // 当前部门和子部门的关系无需在这里删除,因为包含子部门时不能删除父部门。 + SysDeptRelation deptRelation = new SysDeptRelation(); + deptRelation.setDeptId(deptId); + sysDeptRelationMapper.delete(new QueryWrapper<>(deptRelation)); + SysDataPermDept dataPermDept = new SysDataPermDept(); + dataPermDept.setDeptId(deptId); + sysDataPermDeptMapper.delete(new QueryWrapper<>(dataPermDept)); + return true; + } + + @Override + public List getSysDeptList(SysDept filter, String orderBy) { + return sysDeptMapper.getSysDeptList(filter, orderBy); + } + + @Override + public List getSysDeptListWithRelation(SysDept filter, String orderBy) { + List resultList = sysDeptMapper.getSysDeptList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public boolean hasChildren(Long deptId) { + SysDept filter = new SysDept(); + filter.setParentId(deptId); + return getCountByFilter(filter) > 0; + } + + @Override + public boolean hasChildrenUser(Long deptId) { + SysUser sysUser = new SysUser(); + sysUser.setDeptId(deptId); + return sysUserService.getCountByFilter(sysUser) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addSysDeptPostList(List sysDeptPostList, Long deptId) { + for (SysDeptPost sysDeptPost : sysDeptPostList) { + sysDeptPost.setDeptPostId(idGenerator.nextLongId()); + sysDeptPost.setDeptId(deptId); + sysDeptPostMapper.insert(sysDeptPost); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateSysDeptPost(SysDeptPost sysDeptPost) { + SysDeptPost filter = new SysDeptPost(); + filter.setDeptPostId(sysDeptPost.getDeptPostId()); + filter.setDeptId(sysDeptPost.getDeptId()); + filter.setPostId(sysDeptPost.getPostId()); + UpdateWrapper uw = + BaseService.createUpdateQueryForNullValue(sysDeptPost, SysDeptPost.class); + uw.setEntity(filter); + return sysDeptPostMapper.update(sysDeptPost, uw) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeSysDeptPost(Long deptId, Long postId) { + SysDeptPost filter = new SysDeptPost(); + filter.setDeptId(deptId); + filter.setPostId(postId); + return sysDeptPostMapper.delete(new QueryWrapper<>(filter)) > 0; + } + + @Override + public SysDeptPost getSysDeptPost(Long deptId, Long postId) { + SysDeptPost filter = new SysDeptPost(); + filter.setDeptId(deptId); + filter.setPostId(postId); + return sysDeptPostMapper.selectOne(new QueryWrapper<>(filter)); + } + + @Override + public SysDeptPost getSysDeptPost(Long deptPostId) { + return sysDeptPostMapper.selectById(deptPostId); + } + + @Override + public List> getSysDeptPostListWithRelationByDeptId(Long deptId) { + return sysDeptPostMapper.getSysDeptPostListWithRelationByDeptId(deptId); + } + + @Override + public List getSysDeptPostList(Long deptId, Set postIdSet) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDeptPost::getDeptId, deptId); + queryWrapper.in(SysDeptPost::getPostId, postIdSet); + return sysDeptPostMapper.selectList(queryWrapper); + } + + @Override + public List getSiblingSysDeptPostList(Long deptId, Set postIdSet) { + SysDept sysDept = this.getById(deptId); + if (sysDept == null) { + return new LinkedList<>(); + } + List deptList = this.getListByParentId("parentId", sysDept.getParentId()); + Set deptIdSet = deptList.stream().map(SysDept::getDeptId).collect(Collectors.toSet()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysDeptPost::getDeptId, deptIdSet); + queryWrapper.in(SysDeptPost::getPostId, postIdSet); + return sysDeptPostMapper.selectList(queryWrapper); + } + + @Override + public List getLeaderDeptPostIdList(Long deptId) { + List resultList = sysDeptPostMapper.getLeaderDeptPostList(deptId); + return resultList.stream().map(SysDeptPost::getDeptPostId).collect(Collectors.toList()); + } + + @Override + public List getUpLeaderDeptPostIdList(Long deptId) { + SysDept sysDept = this.getById(deptId); + if (sysDept.getParentId() == null) { + return new LinkedList<>(); + } + return this.getLeaderDeptPostIdList(sysDept.getParentId()); + } + + @Override + public List getAllChildDeptIdByParentIds(List parentIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysDeptRelation::getParentDeptId, parentIds); + return sysDeptRelationMapper.selectList(queryWrapper) + .stream().map(SysDeptRelation::getDeptId).collect(Collectors.toList()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java new file mode 100644 index 00000000..35c70a11 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,239 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.webadmin.upms.bo.SysMenuExtraData; +import com.orangeforms.webadmin.upms.dao.SysMenuMapper; +import com.orangeforms.webadmin.upms.dao.SysRoleMenuMapper; +import com.orangeforms.webadmin.upms.model.SysMenu; +import com.orangeforms.webadmin.upms.model.SysRoleMenu; +import com.orangeforms.webadmin.upms.model.constant.SysMenuType; +import com.orangeforms.webadmin.upms.model.constant.SysOnlineMenuPermType; +import com.orangeforms.webadmin.upms.service.SysMenuService; +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.*; +import java.util.stream.Collectors; + +/** + * 菜单数据服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysMenuService") +public class SysMenuServiceImpl extends BaseService implements SysMenuService { + + @Autowired + private SysMenuMapper sysMenuMapper; + @Autowired + private SysRoleMenuMapper sysRoleMenuMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysMenuMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysMenu saveNew(SysMenu sysMenu) { + sysMenu.setMenuId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(sysMenu); + sysMenuMapper.insert(sysMenu); + // 判断当前菜单是否为指向在线表单的菜单,并将根据约定,动态插入两个子菜单。 + if (sysMenu.getOnlineFormId() != null && sysMenu.getOnlineFlowEntryId() == null) { + SysMenu viewSubMenu = new SysMenu(); + viewSubMenu.setMenuId(idGenerator.nextLongId()); + viewSubMenu.setParentId(sysMenu.getMenuId()); + viewSubMenu.setMenuType(SysMenuType.TYPE_BUTTON); + viewSubMenu.setMenuName("查看"); + viewSubMenu.setShowOrder(0); + viewSubMenu.setOnlineFormId(sysMenu.getOnlineFormId()); + viewSubMenu.setOnlineMenuPermType(SysOnlineMenuPermType.TYPE_VIEW); + MyModelUtil.fillCommonsForInsert(viewSubMenu); + sysMenuMapper.insert(viewSubMenu); + SysMenu editSubMenu = new SysMenu(); + editSubMenu.setMenuId(idGenerator.nextLongId()); + editSubMenu.setParentId(sysMenu.getMenuId()); + editSubMenu.setMenuType(SysMenuType.TYPE_BUTTON); + editSubMenu.setMenuName("编辑"); + editSubMenu.setShowOrder(1); + editSubMenu.setOnlineFormId(sysMenu.getOnlineFormId()); + editSubMenu.setOnlineMenuPermType(SysOnlineMenuPermType.TYPE_EDIT); + MyModelUtil.fillCommonsForInsert(editSubMenu); + sysMenuMapper.insert(editSubMenu); + } + return sysMenu; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysMenu sysMenu, SysMenu originalSysMenu) { + MyModelUtil.fillCommonsForUpdate(sysMenu, originalSysMenu); + sysMenu.setMenuType(originalSysMenu.getMenuType()); + UpdateWrapper uw = this.createUpdateQueryForNullValue(sysMenu, sysMenu.getMenuId()); + if (sysMenuMapper.update(sysMenu, uw) != 1) { + return false; + } + // 如果当前菜单的在线表单Id变化了,就需要同步更新他的内置子菜单也同步更新。 + if (ObjectUtil.notEqual(originalSysMenu.getOnlineFormId(), sysMenu.getOnlineFormId())) { + SysMenu onlineSubMenu = new SysMenu(); + onlineSubMenu.setOnlineFormId(sysMenu.getOnlineFormId()); + sysMenuMapper.update(onlineSubMenu, + new QueryWrapper().lambda().eq(SysMenu::getParentId, sysMenu.getMenuId())); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(SysMenu menu) { + Long menuId = menu.getMenuId(); + if (sysMenuMapper.delete(new LambdaQueryWrapper().eq(SysMenu::getMenuId, menuId)) != 1) { + return false; + } + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.delete(new QueryWrapper<>(roleMenu)); + // 如果为指向在线表单的菜单,则连同删除子菜单 + if (menu.getOnlineFormId() != null) { + SysMenu filter = new SysMenu(); + filter.setParentId(menuId); + List childMenus = sysMenuMapper.selectList(new QueryWrapper<>(filter)); + sysMenuMapper.delete(new LambdaQueryWrapper().eq(SysMenu::getParentId, menuId)); + if (CollUtil.isNotEmpty(childMenus)) { + List childMenuIds = childMenus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.in(SysRoleMenu::getMenuId, childMenuIds); + sysRoleMenuMapper.delete(qw); + } + } + return true; + } + + @Override + public Collection getMenuListByUserId(Long userId) { + List menuList = sysMenuMapper.getMenuListByUserId(userId); + return this.distinctMenuList(menuList); + } + + @Override + public Collection getMenuListByRoleIds(String roleIds) { + if (StrUtil.isBlank(roleIds)) { + return CollUtil.empty(Long.class); + } + Set roleIdSet = StrUtil.split(roleIds, ",").stream().map(Long::valueOf).collect(Collectors.toSet()); + List menuList = sysMenuMapper.getMenuListByRoleIds(roleIdSet); + return this.distinctMenuList(menuList); + } + + @Override + public boolean hasChildren(Long menuId) { + SysMenu menu = new SysMenu(); + menu.setParentId(menuId); + return this.getCountByFilter(menu) > 0; + } + + @Override + public CallResult verifyRelatedData(SysMenu sysMenu, SysMenu originalSysMenu) { + // menu、ui fragment和button类型的menu不能没有parentId + if (sysMenu.getParentId() == null && sysMenu.getMenuType() != SysMenuType.TYPE_DIRECTORY) { + return CallResult.error("数据验证失败,当前类型菜单项的上级菜单不能为空!"); + } + if (this.needToVerify(sysMenu, originalSysMenu, SysMenu::getParentId)) { + String errorMessage = checkErrorOfNonDirectoryMenu(sysMenu); + if (errorMessage != null) { + return CallResult.error(errorMessage); + } + } + if (!this.verifyMenuCode(sysMenu, originalSysMenu)) { + return CallResult.error("数据验证失败,菜单编码已存在,不能重复使用!"); + } + return CallResult.ok(); + } + + @Override + public List getAllOnlineMenuList(Integer menuType) { + LambdaQueryWrapper queryWrapper = + new QueryWrapper().lambda().isNotNull(SysMenu::getOnlineFormId); + if (menuType != null) { + queryWrapper.eq(SysMenu::getMenuType, menuType); + } + return sysMenuMapper.selectList(queryWrapper); + } + + private boolean verifyMenuCode(SysMenu sysMenu, SysMenu originalSysMenu) { + if (sysMenu.getExtraData() == null) { + return true; + } + String menuCode = JSON.parseObject(sysMenu.getExtraData(), SysMenuExtraData.class).getMenuCode(); + if (StrUtil.isBlank(menuCode)) { + return true; + } + String originalMenuCode = ""; + if (originalSysMenu != null && originalSysMenu.getExtraData() != null) { + originalMenuCode = JSON.parseObject(originalSysMenu.getExtraData(), SysMenuExtraData.class).getMenuCode(); + } + return StrUtil.equals(menuCode, originalMenuCode) + || sysMenuMapper.countMenuCode("\"menuCode\":\"" + menuCode + "\"") == 0; + } + + private String checkErrorOfNonDirectoryMenu(SysMenu sysMenu) { + // 判断父节点是否存在 + SysMenu parentSysMenu = getById(sysMenu.getParentId()); + if (parentSysMenu == null) { + return "数据验证失败,关联的上级菜单并不存在,请刷新后重试!"; + } + // 逐个判断每种类型的菜单,他的父菜单的合法性,先从目录类型和菜单类型开始 + if (sysMenu.getMenuType() == SysMenuType.TYPE_DIRECTORY + || sysMenu.getMenuType() == SysMenuType.TYPE_MENU) { + // 他们的上级只能是目录 + if (parentSysMenu.getMenuType() != SysMenuType.TYPE_DIRECTORY) { + return "数据验证失败,当前类型菜单项的上级菜单只能是目录类型!"; + } + } else if (sysMenu.getMenuType() == SysMenuType.TYPE_UI_FRAGMENT) { + // ui fragment的上级只能是menu类型 + if (parentSysMenu.getMenuType() != SysMenuType.TYPE_MENU) { + return "数据验证失败,当前类型菜单项的上级菜单只能是菜单类型和按钮类型!"; + } + } else if (sysMenu.getMenuType() == SysMenuType.TYPE_BUTTON) { + // button的上级只能是menu和ui fragment + if (parentSysMenu.getMenuType() != SysMenuType.TYPE_MENU + && parentSysMenu.getMenuType() != SysMenuType.TYPE_UI_FRAGMENT) { + return "数据验证失败,当前类型菜单项的上级菜单只能是菜单类型和UI片段类型!"; + } + } else { + return "数据验证失败,不支持的菜单类型!"; + } + return null; + } + + private Collection distinctMenuList(List menuList) { + LinkedHashMap menuMap = new LinkedHashMap<>(); + for (SysMenu menu : menuList) { + menuMap.put(menu.getMenuId(), menu); + } + return menuMap.values(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java new file mode 100644 index 00000000..69c4abb6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPermWhitelistServiceImpl.java @@ -0,0 +1,47 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.webadmin.upms.dao.SysPermWhitelistMapper; +import com.orangeforms.webadmin.upms.model.SysPermWhitelist; +import com.orangeforms.webadmin.upms.service.SysPermWhitelistService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 权限资源白名单数据服务类。 + * 白名单中的权限资源,可以不受权限控制,任何用户皆可访问,一般用于常用的字典数据列表接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysPermWhitelistService") +public class SysPermWhitelistServiceImpl extends BaseService implements SysPermWhitelistService { + + @Autowired + private SysPermWhitelistMapper sysPermWhitelistMapper; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysPermWhitelistMapper; + } + + @Override + public List getWhitelistPermList() { + List dataList = this.getAllList(); + Function getterFunc = SysPermWhitelist::getPermUrl; + return dataList.stream() + .filter(x -> getterFunc.apply(x) != null).map(getterFunc).collect(Collectors.toList()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java new file mode 100644 index 00000000..edac3465 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysPostServiceImpl.java @@ -0,0 +1,186 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.webadmin.upms.dao.SysDeptPostMapper; +import com.orangeforms.webadmin.upms.dao.SysPostMapper; +import com.orangeforms.webadmin.upms.dao.SysUserPostMapper; +import com.orangeforms.webadmin.upms.model.SysDeptPost; +import com.orangeforms.webadmin.upms.model.SysPost; +import com.orangeforms.webadmin.upms.model.SysUserPost; +import com.orangeforms.webadmin.upms.service.SysDeptService; +import com.orangeforms.webadmin.upms.service.SysPostService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 岗位管理数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysPostService") +public class SysPostServiceImpl extends BaseService implements SysPostService, BizWidgetDatasource { + + @Autowired + private SysPostMapper sysPostMapper; + @Autowired + private SysUserPostMapper sysUserPostMapper; + @Autowired + private SysDeptPostMapper sysDeptPostMapper; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysPostMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_POST_TYPE, this); + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_DEPT_POST_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysPost.class); + SysPost postFilter = filter == null ? null : BeanUtil.toBean(filter, SysPost.class); + if (StrUtil.equals(type, BizWidgetDatasourceType.UPMS_POST_TYPE)) { + List postList = this.getSysPostList(postFilter, orderBy); + return MyPageUtil.makeResponseData(postList, BeanUtil::beanToMap); + } + Assert.notNull(filter, "filter can't be NULL."); + Long deptId = (Long) filter.get("deptId"); + List> dataList = sysDeptService.getSysDeptPostListWithRelationByDeptId(deptId); + return MyPageUtil.makeResponseData(dataList); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List postList; + if (StrUtil.isBlank(fieldName)) { + postList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + postList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysPost.class, fieldName, fieldValues)); + } + return MyModelUtil.beanToMapList(postList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysPost saveNew(SysPost sysPost) { + sysPost.setPostId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(sysPost); + MyModelUtil.setDefaultValue(sysPost, "leaderPost", false); + sysPostMapper.insert(sysPost); + return sysPost; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysPost sysPost, SysPost originalSysPost) { + MyModelUtil.fillCommonsForUpdate(sysPost, originalSysPost); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(sysPost, sysPost.getPostId()); + return sysPostMapper.update(sysPost, uw) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long postId) { + if (sysPostMapper.deleteById(postId) != 1) { + return false; + } + // 开始删除多对多父表的关联 + SysUserPost sysUserPost = new SysUserPost(); + sysUserPost.setPostId(postId); + sysUserPostMapper.delete(new QueryWrapper<>(sysUserPost)); + SysDeptPost sysDeptPost = new SysDeptPost(); + sysDeptPost.setPostId(postId); + sysDeptPostMapper.delete(new QueryWrapper<>(sysDeptPost)); + return true; + } + + @Override + public List getSysPostList(SysPost filter, String orderBy) { + return sysPostMapper.getSysPostList(filter, orderBy); + } + + @Override + public List getSysPostListWithRelation(SysPost filter, String orderBy) { + List resultList = sysPostMapper.getSysPostList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public List getNotInSysPostListByDeptId(Long deptId, SysPost filter, String orderBy) { + List resultList = sysPostMapper.getNotInSysPostListByDeptId(deptId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + @Override + public List getSysPostListByDeptId(Long deptId, SysPost filter, String orderBy) { + List resultList = sysPostMapper.getSysPostListByDeptId(deptId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + @Override + public List getSysUserPostListByUserId(Long userId) { + SysUserPost filter = new SysUserPost(); + filter.setUserId(userId); + return sysUserPostMapper.selectList(new QueryWrapper<>(filter)); + } + + @Override + public boolean existAllPrimaryKeys(Set deptPostIdSet, Long deptId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDeptPost::getDeptId, deptId); + queryWrapper.in(SysDeptPost::getDeptPostId, deptPostIdSet); + return sysDeptPostMapper.selectCount(queryWrapper) == deptPostIdSet.size(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java new file mode 100644 index 00000000..1edc6b44 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,192 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.webadmin.upms.dao.SysRoleMapper; +import com.orangeforms.webadmin.upms.dao.SysRoleMenuMapper; +import com.orangeforms.webadmin.upms.dao.SysUserRoleMapper; +import com.orangeforms.webadmin.upms.model.SysRole; +import com.orangeforms.webadmin.upms.model.SysRoleMenu; +import com.orangeforms.webadmin.upms.model.SysUserRole; +import com.orangeforms.webadmin.upms.service.SysMenuService; +import com.orangeforms.webadmin.upms.service.SysRoleService; +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 jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 角色数据服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysRoleService") +public class SysRoleServiceImpl extends BaseService implements SysRoleService, BizWidgetDatasource { + + @Autowired + private SysRoleMapper sysRoleMapper; + @Autowired + private SysRoleMenuMapper sysRoleMenuMapper; + @Autowired + private SysUserRoleMapper sysUserRoleMapper; + @Autowired + private SysMenuService sysMenuService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回主对象的Mapper对象。 + * + * @return 主对象的Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysRoleMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_ROLE_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysRole.class); + SysRole roleFilter = filter == null ? null : BeanUtil.toBean(filter, SysRole.class); + List roleList = this.getSysRoleList(roleFilter, orderBy); + return MyPageUtil.makeResponseData(roleList, BeanUtil::beanToMap); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List roleList; + if (StrUtil.isBlank(fieldName)) { + roleList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + roleList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysRole.class, fieldName, fieldValues)); + } + return MyModelUtil.beanToMapList(roleList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysRole saveNew(SysRole role, Set menuIdSet) { + role.setRoleId(idGenerator.nextLongId()); + MyModelUtil.fillCommonsForInsert(role); + sysRoleMapper.insert(role); + if (menuIdSet != null) { + for (Long menuId : menuIdSet) { + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setRoleId(role.getRoleId()); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.insert(roleMenu); + } + } + return role; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysRole role, SysRole originalRole, Set menuIdSet) { + MyModelUtil.fillCommonsForUpdate(role, originalRole); + if (sysRoleMapper.updateById(role) != 1) { + return false; + } + SysRoleMenu deletedRoleMenu = new SysRoleMenu(); + deletedRoleMenu.setRoleId(role.getRoleId()); + sysRoleMenuMapper.delete(new QueryWrapper<>(deletedRoleMenu)); + if (menuIdSet != null) { + for (Long menuId : menuIdSet) { + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setRoleId(role.getRoleId()); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.insert(roleMenu); + } + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long roleId) { + if (sysRoleMapper.deleteById(roleId) != 1) { + return false; + } + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setRoleId(roleId); + sysRoleMenuMapper.delete(new QueryWrapper<>(roleMenu)); + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(roleId); + sysUserRoleMapper.delete(new QueryWrapper<>(userRole)); + return true; + } + + @Override + public List getSysRoleList(SysRole filter, String orderBy) { + return sysRoleMapper.getSysRoleList(filter, orderBy); + } + + @Override + public List getSysUserRoleListByUserId(Long userId) { + SysUserRole filter = new SysUserRole(); + filter.setUserId(userId); + return sysUserRoleMapper.selectList(new QueryWrapper<>(filter)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addUserRoleList(List userRoleList) { + for (SysUserRole userRole : userRoleList) { + sysUserRoleMapper.insert(userRole); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeUserRole(Long roleId, Long userId) { + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(roleId); + userRole.setUserId(userId); + return sysUserRoleMapper.delete(new QueryWrapper<>(userRole)) == 1; + } + + @Override + public CallResult verifyRelatedData(SysRole sysRole, SysRole originalSysRole, String menuIdListString) { + JSONObject jsonObject = null; + if (StringUtils.isNotBlank(menuIdListString)) { + Set menuIdSet = Arrays.stream( + menuIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysMenuService.existAllPrimaryKeys(menuIdSet)) { + return CallResult.error("数据验证失败,存在不合法的菜单权限,请刷新后重试!"); + } + jsonObject = new JSONObject(); + jsonObject.put("menuIdSet", menuIdSet); + } + return CallResult.ok(jsonObject); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java new file mode 100644 index 00000000..4e806bbe --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/service/impl/SysUserServiceImpl.java @@ -0,0 +1,384 @@ +package com.orangeforms.webadmin.upms.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.*; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.webadmin.upms.service.*; +import com.orangeforms.webadmin.upms.dao.*; +import com.orangeforms.webadmin.upms.model.*; +import com.orangeforms.webadmin.upms.model.constant.SysUserStatus; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.constant.BizWidgetDatasourceType; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.constant.UserFilterGroup; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 用户管理数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Service("sysUserService") +public class SysUserServiceImpl extends BaseService implements SysUserService, BizWidgetDatasource { + + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private SysUserMapper sysUserMapper; + @Autowired + private SysUserRoleMapper sysUserRoleMapper; + @Autowired + private SysUserPostMapper sysUserPostMapper; + @Autowired + private SysDataPermUserMapper sysDataPermUserMapper; + @Autowired + private SysDeptService sysDeptService; + @Autowired + private SysRoleService sysRoleService; + @Autowired + private SysDataPermService sysDataPermService; + @Autowired + private SysPostService sysPostService; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return sysUserMapper; + } + + @PostConstruct + private void registerBizWidgetDatasource() { + bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_USER_TYPE, this); + } + + @Override + public MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + List userList = null; + String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class, false); + SysUser userFilter = BeanUtil.toBean(filter, SysUser.class); + if (filter != null) { + Object group = filter.get("USER_FILTER_GROUP"); + if (group != null) { + JSONObject filterGroupJson = JSON.parseObject(group.toString()); + String groupType = filterGroupJson.getString("type"); + String values = filterGroupJson.getString("values"); + if (UserFilterGroup.USER.equals(groupType)) { + List loginNames = StrUtil.splitTrim(values, ","); + userList = sysUserMapper.getSysUserListByLoginNames(loginNames, userFilter, orderBy); + } else { + Set groupIds = StrUtil.splitTrim(values, ",") + .stream().map(Long::valueOf).collect(Collectors.toSet()); + userList = this.getUserListByGroupIds(groupType, groupIds, userFilter, orderBy); + } + } + } + if (userList == null) { + userList = this.getSysUserList(userFilter, orderBy); + } + this.buildRelationForDataList(userList, MyRelationParam.dictOnly()); + return MyPageUtil.makeResponseData(userList, BeanUtil::beanToMap); + } + + private List getUserListByGroupIds(String groupType, Set groupIds, SysUser filter, String orderBy) { + if (groupType.equals(UserFilterGroup.DEPT)) { + return sysUserMapper.getSysUserListByDeptIds(groupIds, filter, orderBy); + } + List userIds = null; + switch (groupType) { + case UserFilterGroup.ROLE: + userIds = sysUserMapper.getUserIdListByRoleIds(groupIds, filter, orderBy); + break; + case UserFilterGroup.POST: + userIds = sysUserMapper.getUserIdListByPostIds(groupIds, filter, orderBy); + break; + case UserFilterGroup.DEPT_POST: + userIds = sysUserMapper.getUserIdListByDeptPostIds(groupIds, filter, orderBy); + break; + default: + break; + } + if (CollUtil.isEmpty(userIds)) { + return CollUtil.empty(SysUser.class); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getUserId, userIds); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } + return sysUserMapper.selectList(queryWrapper); + } + + @Override + public List> getDataListWithInList(String type, String fieldName, List fieldValues) { + List userList; + if (StrUtil.isBlank(fieldName)) { + userList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet())); + } else { + userList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysUser.class, fieldName, fieldValues)); + } + this.buildRelationForDataList(userList, MyRelationParam.dictOnly()); + return MyModelUtil.beanToMapList(userList); + } + + /** + * 获取指定登录名的用户对象。 + * + * @param loginName 指定登录用户名。 + * @return 用户对象。 + */ + @Override + public SysUser getSysUserByLoginName(String loginName) { + SysUser filter = new SysUser(); + filter.setLoginName(loginName); + return sysUserMapper.selectOne(new QueryWrapper<>(filter)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysUser saveNew(SysUser user, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet) { + user.setUserId(idGenerator.nextLongId()); + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setUserStatus(SysUserStatus.STATUS_NORMAL); + user.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.fillCommonsForInsert(user); + sysUserMapper.insert(user); + if (CollUtil.isNotEmpty(deptPostIdSet)) { + for (Long deptPostId : deptPostIdSet) { + SysDeptPost deptPost = sysDeptService.getSysDeptPost(deptPostId); + SysUserPost userPost = new SysUserPost(); + userPost.setUserId(user.getUserId()); + userPost.setDeptPostId(deptPostId); + userPost.setPostId(deptPost.getPostId()); + sysUserPostMapper.insert(userPost); + } + } + if (CollUtil.isNotEmpty(roleIdSet)) { + for (Long roleId : roleIdSet) { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(user.getUserId()); + userRole.setRoleId(roleId); + sysUserRoleMapper.insert(userRole); + } + } + if (CollUtil.isNotEmpty(dataPermIdSet)) { + for (Long dataPermId : dataPermIdSet) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(user.getUserId()); + sysDataPermUserMapper.insert(dataPermUser); + } + } + return user; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(SysUser user, SysUser originalUser, Set roleIdSet, Set deptPostIdSet, Set dataPermIdSet) { + user.setLoginName(originalUser.getLoginName()); + user.setPassword(originalUser.getPassword()); + MyModelUtil.fillCommonsForUpdate(user, originalUser); + UpdateWrapper uw = this.createUpdateQueryForNullValue(user, user.getUserId()); + if (sysUserMapper.update(user, uw) != 1) { + return false; + } + // 先删除原有的User-Post关联关系,再重新插入新的关联关系 + SysUserPost deletedUserPost = new SysUserPost(); + deletedUserPost.setUserId(user.getUserId()); + sysUserPostMapper.delete(new QueryWrapper<>(deletedUserPost)); + if (CollUtil.isNotEmpty(deptPostIdSet)) { + for (Long deptPostId : deptPostIdSet) { + SysDeptPost deptPost = sysDeptService.getSysDeptPost(deptPostId); + SysUserPost userPost = new SysUserPost(); + userPost.setUserId(user.getUserId()); + userPost.setDeptPostId(deptPostId); + userPost.setPostId(deptPost.getPostId()); + sysUserPostMapper.insert(userPost); + } + } + // 先删除原有的User-Role关联关系,再重新插入新的关联关系 + SysUserRole deletedUserRole = new SysUserRole(); + deletedUserRole.setUserId(user.getUserId()); + sysUserRoleMapper.delete(new QueryWrapper<>(deletedUserRole)); + if (CollUtil.isNotEmpty(roleIdSet)) { + for (Long roleId : roleIdSet) { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(user.getUserId()); + userRole.setRoleId(roleId); + sysUserRoleMapper.insert(userRole); + } + } + // 先删除原有的DataPerm-User关联关系,在重新插入新的关联关系 + SysDataPermUser deletedDataPermUser = new SysDataPermUser(); + deletedDataPermUser.setUserId(user.getUserId()); + sysDataPermUserMapper.delete(new QueryWrapper<>(deletedDataPermUser)); + if (CollUtil.isNotEmpty(dataPermIdSet)) { + for (Long dataPermId : dataPermIdSet) { + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setDataPermId(dataPermId); + dataPermUser.setUserId(user.getUserId()); + sysDataPermUserMapper.insert(dataPermUser); + } + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean changePassword(Long userId, String newPass) { + SysUser updatedUser = new SysUser(); + updatedUser.setUserId(userId); + updatedUser.setPassword(passwordEncoder.encode(newPass)); + return sysUserMapper.updateById(updatedUser) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean changeHeadImage(Long userId, String newHeadImage) { + SysUser updatedUser = new SysUser(); + updatedUser.setUserId(userId); + updatedUser.setHeadImageUrl(newHeadImage); + return sysUserMapper.updateById(updatedUser) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long userId) { + if (sysUserMapper.deleteById(userId) == 0) { + return false; + } + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(userId); + sysUserRoleMapper.delete(new QueryWrapper<>(userRole)); + SysUserPost userPost = new SysUserPost(); + userPost.setUserId(userId); + sysUserPostMapper.delete(new QueryWrapper<>(userPost)); + SysDataPermUser dataPermUser = new SysDataPermUser(); + dataPermUser.setUserId(userId); + sysDataPermUserMapper.delete(new QueryWrapper<>(dataPermUser)); + return true; + } + + @Override + public List getSysUserList(SysUser filter, String orderBy) { + return sysUserMapper.getSysUserList(filter, orderBy); + } + + @Override + public List getSysUserListWithRelation(SysUser filter, String orderBy) { + List resultList = sysUserMapper.getSysUserList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public List getSysUserListByRoleId(Long roleId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByRoleId(roleId, filter, orderBy); + } + + @Override + public List getNotInSysUserListByRoleId(Long roleId, SysUser filter, String orderBy) { + return sysUserMapper.getNotInSysUserListByRoleId(roleId, filter, orderBy); + } + + @Override + public List getSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByDataPermId(dataPermId, filter, orderBy); + } + + @Override + public List getNotInSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy) { + return sysUserMapper.getNotInSysUserListByDataPermId(dataPermId, filter, orderBy); + } + + @Override + public List getSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByDeptPostId(deptPostId, filter, orderBy); + } + + @Override + public List getNotInSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy) { + return sysUserMapper.getNotInSysUserListByDeptPostId(deptPostId, filter, orderBy); + } + + @Override + public List getSysUserListByPostId(Long postId, SysUser filter, String orderBy) { + return sysUserMapper.getSysUserListByPostId(postId, filter, orderBy); + } + + @Override + public CallResult verifyRelatedData( + SysUser sysUser, SysUser originalSysUser, String roleIds, String deptPostIds, String dataPermIds) { + JSONObject jsonObject = new JSONObject(); + if (StrUtil.isBlank(deptPostIds)) { + return CallResult.error("数据验证失败,用户的部门岗位数据不能为空!"); + } + Set deptPostIdSet = + Arrays.stream(deptPostIds.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysPostService.existAllPrimaryKeys(deptPostIdSet, sysUser.getDeptId())) { + return CallResult.error("数据验证失败,存在不合法的用户岗位,请刷新后重试!"); + } + jsonObject.put("deptPostIdSet", deptPostIdSet); + if (StrUtil.isBlank(roleIds)) { + return CallResult.error("数据验证失败,用户的角色数据不能为空!"); + } + Set roleIdSet = Arrays.stream( + roleIds.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysRoleService.existAllPrimaryKeys(roleIdSet)) { + return CallResult.error("数据验证失败,存在不合法的用户角色,请刷新后重试!"); + } + jsonObject.put("roleIdSet", roleIdSet); + if (StrUtil.isBlank(dataPermIds)) { + return CallResult.error("数据验证失败,用户的数据权限不能为空!"); + } + Set dataPermIdSet = Arrays.stream( + dataPermIds.split(",")).map(Long::valueOf).collect(Collectors.toSet()); + if (!sysDataPermService.existAllPrimaryKeys(dataPermIdSet)) { + return CallResult.error("数据验证失败,存在不合法的数据权限,请刷新后重试!"); + } + jsonObject.put("dataPermIdSet", dataPermIdSet); + //这里是基于字典的验证。 + if (this.needToVerify(sysUser, originalSysUser, SysUser::getDeptId) + && !sysDeptService.existId(sysUser.getDeptId())) { + return CallResult.error("数据验证失败,关联的用户部门Id并不存在,请刷新后重试!"); + } + return CallResult.ok(jsonObject); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java new file mode 100644 index 00000000..601dc7c2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermDeptVo.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与部门关联VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与部门关联VO") +@Data +public class SysDataPermDeptVo { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id") + private Long dataPermId; + + /** + * 关联部门Id。 + */ + @Schema(description = "关联部门Id") + private Long deptId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java new file mode 100644 index 00000000..7e4bc12c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermMenuVo.java @@ -0,0 +1,27 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 数据权限与菜单关联VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限与菜单关联VO") +@Data +public class SysDataPermMenuVo { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id") + private Long dataPermId; + + /** + * 关联菜单Id。 + */ + @Schema(description = "关联菜单Id") + private Long menuId; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java new file mode 100644 index 00000000..e07af624 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDataPermVo.java @@ -0,0 +1,57 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +/** + * 数据权限VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "数据权限VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysDataPermVo extends BaseVo { + + /** + * 数据权限Id。 + */ + @Schema(description = "数据权限Id") + private Long dataPermId; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称") + private String dataPermName; + + /** + * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。 + */ + @Schema(description = "数据权限规则类型") + private Integer ruleType; + + /** + * 部门Id列表(逗号分隔)。 + */ + @Schema(description = "部门Id列表") + private String deptIdListString; + + /** + * 数据权限与部门关联对象列表。 + */ + @Schema(description = "数据权限与部门关联对象列表") + private List> dataPermDeptList; + + /** + * 数据权限与菜单关联对象列表。 + */ + @Schema(description = "数据权限与菜单关联对象列表") + private List> dataPermMenuList; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java new file mode 100644 index 00000000..6e502095 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptPostVo.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 部门岗位VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "部门岗位VO") +@Data +public class SysDeptPostVo { + + /** + * 部门岗位Id。 + */ + @Schema(description = "部门岗位Id") + private Long deptPostId; + + /** + * 部门Id。 + */ + @Schema(description = "部门Id") + private Long deptId; + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id") + private Long postId; + + /** + * 部门岗位显示名称。 + */ + @Schema(description = "部门岗位显示名称") + private String postShowName; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java new file mode 100644 index 00000000..1f08901f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysDeptVo.java @@ -0,0 +1,65 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 部门管理VO视图对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysDeptVO视图对象") +@Data +public class SysDeptVo { + + /** + * 部门Id。 + */ + @Schema(description = "部门Id") + private Long deptId; + + /** + * 部门名称。 + */ + @Schema(description = "部门名称") + private String deptName; + + /** + * 显示顺序。 + */ + @Schema(description = "显示顺序") + private Integer showOrder; + + /** + * 父部门Id。 + */ + @Schema(description = "父部门Id") + private Long parentId; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java new file mode 100644 index 00000000..e278c859 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysMenuVo.java @@ -0,0 +1,90 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "菜单VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysMenuVo extends BaseVo { + + /** + * 菜单Id。 + */ + @Schema(description = "菜单Id") + private Long menuId; + + /** + * 父菜单Id,目录菜单的父菜单为null + */ + @Schema(description = "父菜单Id") + private Long parentId; + + /** + * 菜单显示名称。 + */ + @Schema(description = "菜单显示名称") + private String menuName; + + /** + * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。 + */ + @Schema(description = "菜单类型") + private Integer menuType; + + /** + * 前端表单路由名称,仅用于menu_type为1的菜单类型。 + */ + @Schema(description = "前端表单路由名称") + private String formRouterName; + + /** + * 在线表单主键Id,仅用于在线表单绑定的菜单。 + */ + @Schema(description = "在线表单主键Id") + private Long onlineFormId; + + /** + * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。 + */ + @Schema(description = "在线表单菜单的权限控制类型") + private Integer onlineMenuPermType; + + /** + * 统计页面主键Id,仅用于统计页面绑定的菜单。 + */ + @Schema(description = "统计页面主键Id") + private Long reportPageId; + + /** + * 仅用于在线表单的流程Id。 + */ + @Schema(description = "仅用于在线表单的流程Id") + private Long onlineFlowEntryId; + + /** + * 菜单显示顺序 (值越小,排序越靠前)。 + */ + @Schema(description = "菜单显示顺序") + private Integer showOrder; + + /** + * 菜单图标。 + */ + @Schema(description = "菜单显示图标") + private String icon; + + /** + * 附加信息。 + */ + @Schema(description = "附加信息") + private String extraData; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java new file mode 100644 index 00000000..15a5f2c7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysPostVo.java @@ -0,0 +1,50 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * 岗位VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "岗位VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysPostVo extends BaseVo { + + /** + * 岗位Id。 + */ + @Schema(description = "岗位Id") + private Long postId; + + /** + * 岗位名称。 + */ + @Schema(description = "岗位名称") + private String postName; + + /** + * 岗位层级,数值越小级别越高。 + */ + @Schema(description = "岗位层级,数值越小级别越高") + private Integer postLevel; + + /** + * 是否领导岗位。 + */ + @Schema(description = "是否领导岗位") + private Boolean leaderPost; + + /** + * postId 的多对多关联表数据对象,数据对应类型为SysDeptPostVo。 + */ + @Schema(description = "postId 的多对多关联表数据对象") + private Map sysDeptPost; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java new file mode 100644 index 00000000..0aaf0358 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysRoleVo.java @@ -0,0 +1,39 @@ +package com.orangeforms.webadmin.upms.vo; + +import com.orangeforms.common.core.base.vo.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +/** + * 角色VO。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "角色VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class SysRoleVo extends BaseVo { + + /** + * 角色Id。 + */ + @Schema(description = "角色Id") + private Long roleId; + + /** + * 角色名称。 + */ + @Schema(description = "角色名称") + private String roleName; + + /** + * 角色与菜单关联对象列表。 + */ + @Schema(description = "角色与菜单关联对象列表") + private List> sysRoleMenuList; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java new file mode 100644 index 00000000..194e8d86 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/java/com/orangeforms/webadmin/upms/vo/SysUserVo.java @@ -0,0 +1,133 @@ +package com.orangeforms.webadmin.upms.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; +import java.util.List; + +/** + * 用户管理VO视图对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "SysUserVO视图对象") +@Data +public class SysUserVo { + + /** + * 用户Id。 + */ + @Schema(description = "用户Id") + private Long userId; + + /** + * 登录用户名。 + */ + @Schema(description = "登录用户名") + private String loginName; + + /** + * 用户部门Id。 + */ + @Schema(description = "用户部门Id") + private Long deptId; + + /** + * 用户显示名称。 + */ + @Schema(description = "用户显示名称") + private String showName; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)") + private Integer userType; + + /** + * 用户头像的Url。 + */ + @Schema(description = "用户头像的Url") + private String headImageUrl; + + /** + * 用户状态(0: 正常 1: 锁定)。 + */ + @Schema(description = "用户状态(0: 正常 1: 锁定)") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + @Schema(description = "用户邮箱") + private String email; + + /** + * 用户手机。 + */ + @Schema(description = "用户手机") + private String mobile; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 多对多用户岗位数据集合。 + */ + @Schema(description = "多对多用户岗位数据集合") + private List> sysUserPostList; + + /** + * 多对多用户角色数据集合。 + */ + @Schema(description = "多对多用户角色数据集合") + private List> sysUserRoleList; + + /** + * 多对多用户数据权限数据集合。 + */ + @Schema(description = "多对多用户数据权限数据集合") + private List> sysDataPermUserList; + + /** + * deptId 字典关联数据。 + */ + @Schema(description = "deptId 字典关联数据") + private Map deptIdDictMap; + + /** + * userType 常量字典关联数据。 + */ + @Schema(description = "userType 常量字典关联数据") + private Map userTypeDictMap; + + /** + * userStatus 常量字典关联数据。 + */ + @Schema(description = "userStatus 常量字典关联数据") + private Map userStatusDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application-dev.yml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application-dev.yml new file mode 100644 index 00000000..1e8f3091 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application-dev.yml @@ -0,0 +1,169 @@ +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + druid: + # 数据库链接 [主数据源] + main: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + # 默认生成的操作日志数据源配置。 + operation-log: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + # 默认生成的全局编码字典数据源配置。 + global-dict: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + # 默认生成的工作流及在线表单数据源配置。 + common-flow-online: + url: jdbc:mysql://localhost:3306/zzdemo-online-open?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai + username: root + password: 123456 + driverClassName: com.mysql.cj.jdbc.Driver + name: application-webadmin + initialSize: 10 + minIdle: 10 + maxActive: 50 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + maxOpenPreparedStatements: 20 + validationQuery: SELECT 'x' + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 + filters: stat,wall + useGlobalDataSourceStat: true + web-stat-filter: + enabled: true + url-pattern: /* + exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*,/actuator/*" + stat-view-servlet: + enabled: true + urlPattern: /druid/* + resetEnable: true + +application: + # 初始化密码。 + defaultUserPassword: 123456 + # 缺省的文件上传根目录。 + uploadFileBaseDir: ./zz-resource/upload-files/app + # 跨域的IP(http://192.168.10.10:8086)白名单列表,多个IP之间逗号分隔(* 表示全部信任,空白表示禁用跨域信任)。 + credentialIpList: "*" + # Session的用户和数据权限在Redis中的过期时间(秒)。一定要和sa-token.timeout + sessionExpiredSeconds: 86400 + # 是否排他登录。 + excludeLogin: false + +# 这里仅仅是一个第三方配置的示例,如果没有接入斯三方系统, +# 这里的配置项也不会影响到系统的行为,如果觉得多余,也可以手动删除。 +third-party: + # 第三方系统接入的用户鉴权配置。 + auth: + - appCode: ruoyi + # 访问第三方系统接口的URL前缀,橙单会根据功能添加接口路径的其余部分, + # 比如获取用户Token的接口 http://localhost:8083/orangePluginTest/getTokenData + baseUrl: http://localhost:8083/orangePlugin + # 第三方返回的用户Token数据的缓存过期时长,单位秒。 + # 如果为0,则不缓存,每次涉及第三方的请求,都会发出http请求,交由第三方验证,这样对系统性能会有影响。 + tokenExpiredSeconds: 60 + # 第三方返回的权限数据的缓存过期时长,单位秒。 + permExpiredSeconds: 86400 + +# 这里仅仅是一个第三方配置的示例,如果没有接入斯三方系统, +# 这里的配置项也不会影响到系统的行为,如果觉得多余,也可以手动删除。 +common-ext: + urlPrefix: /admin/commonext + # 这里可以配置多个第三方应用,这里的应用数量,通常会和上面third-party.auth的配置数量一致。 + apps: + # 应用唯一编码,尽量不要使用中文。 + - appCode: ruoyi + # 业务组件的数据源配置。 + bizWidgetDatasources: + # 组件的类型,多个类型之间可以逗号分隔。 + - types: upms_user,upms_dept,upms_role,upms_post,upms_dept_post + # 组件获取列表数据的接口地址。 + listUrl: http://localhost:8083/orangePlugin/listBizWidgetData + # 组件获取详情数据的接口地址。 + viewUrl: http://localhost:8083/orangePlugin/viewBizWidgetData + +common-sequence: + # Snowflake 分布式Id生成算法所需的WorkNode参数值。 + snowflakeWorkNode: 1 + +# 存储session数据的Redis,所有服务均需要,因此放到公共配置中。 +# 根据实际情况,该Redis也可以用于存储其他数据。 +common-redis: + # redisson的配置。每个服务可以自己的配置文件中覆盖此选项。 + redisson: + # 如果该值为false,系统将不会创建RedissionClient的bean。 + enabled: true + # mode的可用值为,single/cluster/sentinel/master-slave + mode: single + # single: 单机模式 + # address: redis://localhost:6379 + # cluster: 集群模式 + # 每个节点逗号分隔,同时每个节点前必须以redis://开头。 + # address: redis://localhost:6379,redis://localhost:6378,... + # sentinel: + # 每个节点逗号分隔,同时每个节点前必须以redis://开头。 + # address: redis://localhost:6379,redis://localhost:6378,... + # master-slave: + # 每个节点逗号分隔,第一个为主节点,其余为从节点。同时每个节点前必须以redis://开头。 + # address: redis://localhost:6379,redis://localhost:6378,... + address: redis://localhost:6379 + # 链接超时,单位毫秒。 + timeout: 6000 + # 单位毫秒。分布式锁的超时检测时长。 + # 如果一次锁内操作超该毫秒数,或在释放锁之前异常退出,Redis会在该时长之后主动删除该锁使用的key。 + lockWatchdogTimeout: 60000 + # redis 密码,空可以不填。 + password: + pool: + # 连接池数量。 + poolSize: 20 + # 连接池中最小空闲数量。 + minIdle: 5 + +minio: + enabled: false + endpoint: http://localhost:19000 + accessKey: admin + secretKey: admin123456 + bucketName: application + +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: Authorization + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: ${application.sessionExpiredSeconds} + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + # 配置 Sa-Token 单独使用的 Redis 连接 + alone-redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: localhost + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + is-read-header: true + is-read-cookie: false diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application.yml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application.yml new file mode 100644 index 00000000..098e90a4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/application.yml @@ -0,0 +1,165 @@ +logging: + level: + # 这里设置的日志级别优先于logback-spring.xml文件Loggers中的日志级别。 + com.orangeforms: info + config: classpath:logback-spring.xml + +server: + port: 8082 + tomcat: + uri-encoding: UTF-8 + threads: + max: 100 + min-spare: 10 + servlet: + encoding: + force: true + charset: UTF-8 + enabled: true + +# spring相关配置 +spring: + application: + name: application-webadmin + profiles: + active: dev + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB + mvc: + converters: + preferred-json-mapper: fastjson + main: + allow-circular-references: true + groovy: + template: + check-template-location: false + +flowable: + async-executor-activate: false + database-schema-update: false + +mybatis-plus: + mapper-locations: classpath:com/orangeforms/webadmin/*/dao/mapper/*Mapper.xml,com/orangeforms/common/log/dao/mapper/*Mapper.xml,com/orangeforms/common/online/dao/mapper/*Mapper.xml,com/orangeforms/common/flow/dao/mapper/*Mapper.xml + type-aliases-package: com.orangeforms.webadmin.*.model,com.orangeforms.common.log.model,com.orangeforms.common.online.model,com.orangeforms.common.flow.model + global-config: + db-config: + logic-delete-value: -1 + logic-not-delete-value: 1 + +# 自动分页的配置 +pagehelper: + helperDialect: mysql + reasonable: true + supportMethodsArguments: false + params: count=countSql + +common-core: + # 可选值为 mysql / postgresql / oracle / dm8 / kingbase / opengauss + databaseType: mysql + +common-online: + # 注意不要以反斜杠(/)结尾。 + urlPrefix: /admin/online + # 打印接口的路径,不要以反斜杠(/)结尾。 + printUrlPath: /admin/report/reportPrint/print + # 在线表单业务数据上传资源路径 + uploadFileBaseDir: ./zz-resource/upload-files/online + # 如果为false,在线表单模块中所有Controller接口将不能使用。 + operationEnabled: true + # 1: minio 2: aliyun-oss 3: qcloud-cos。 + distributeStoreType: 1 + # 调用render接口时候,是否打开一级缓存加速。 + enableRenderCache: false + # 业务表和在线表单内置表是否跨库。 + enabledMultiDatabaseWrite: true + # 脱敏字段的掩码字符,只能为单个字符。 + maskChar: '*' + # 下面的url列表,请保持反斜杠(/)结尾。 + viewUrlList: + - ${common-online.urlPrefix}/onlineOperation/viewByDatasourceId/ + - ${common-online.urlPrefix}/onlineOperation/viewByOneToManyRelationId/ + - ${common-online.urlPrefix}/onlineOperation/listByDatasourceId/ + - ${common-online.urlPrefix}/onlineOperation/listByOneToManyRelationId/ + - ${common-online.urlPrefix}/onlineOperation/exportByDatasourceId/ + - ${common-online.urlPrefix}/onlineOperation/exportByOneToManyRelationId/ + - ${common-online.urlPrefix}/onlineOperation/downloadDatasource/ + - ${common-online.urlPrefix}/onlineOperation/downloadOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/print/ + editUrlList: + - ${common-online.urlPrefix}/onlineOperation/addDatasource/ + - ${common-online.urlPrefix}/onlineOperation/addOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/updateDatasource/ + - ${common-online.urlPrefix}/onlineOperation/updateOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/deleteDatasource/ + - ${common-online.urlPrefix}/onlineOperation/deleteOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/deleteBatchDatasource/ + - ${common-online.urlPrefix}/onlineOperation/deleteBatchOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/uploadDatasource/ + - ${common-online.urlPrefix}/onlineOperation/uploadOneToManyRelation/ + - ${common-online.urlPrefix}/onlineOperation/importDatasource/ + +common-flow: + # 请慎重修改urlPrefix的缺省配置,注意不要以反斜杠(/)结尾。如必须修改其他路径,请同步修改数据库脚本。 + urlPrefix: /admin/flow + # 如果为false,流程模块的所有Controller中的接口将不能使用。 + operationEnabled: true + +common-swagger: + # 当enabled为false的时候,则可禁用swagger。 + enabled: true + # 工程的基础包名。 + basePackage: com.orangeforms + # 工程服务的基础包名。 + serviceBasePackage: com.orangeforms.webadmin + title: 橙单单体服务工程 + description: 橙单单体服务工程详情 + version: 1.0 + +springdoc: + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha + #operations-sorter: order + api-docs: + path: /v3/api-docs + default-flat-param-object: false + +common-datafilter: + tenant: + # 对于单体服务,该值始终为false。 + enabled: false + dataperm: + enabled: true + # 在拼接数据权限过滤的SQL时,我们会用到sys_dept_relation表,该表的前缀由此配置项指定。 + # 如果没有前缀,请使用 "" 。 + deptRelationTablePrefix: zz_ + # 是否在每次执行数据权限查询过滤时,都要进行菜单Id和URL之间的越权验证。如果使用SaToken权限框架,该参数必须为false。 + enableMenuPermVerify: false + +# 暴露监控端点 +management: + endpoints: + web: + exposure: + include: '*' + jmx: + exposure: + include: '*' + endpoint: + # 与中间件相关的健康详情也会被展示 + health: + show-details: always + configprops: + # 在/actuator/configprops中,所有包含password的配置,将用 * 隐藏。 + # 如果不想隐藏任何配置项的值,可以直接使用如下被注释的空值。 + # keys-to-sanitize: + keys-to-sanitize: password + server: + base-path: "/" + +common-log: + # 操作日志配置,对应配置文件common-log/OperationLogProperties.java + operation-log: + enabled: true diff --git a/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/logback-spring.xml b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..6bc0eafb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/application-webadmin/src/main/resources/logback-spring.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + ${LOG_PATTERN} + + + + + + + ${LOG_HOME}/${LOG_NAME}.log + true + + + ${LOG_HOME}/${LOG_NAME}-%d{yyyy-MM-dd}-%i.log + + + 31 + + + 20MB + + + + + ${LOG_PATTERN_EX} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/.DS_Store b/OrangeFormsOpen-MybatisPlus/common/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dcc80517cb460de5168ed2f33090a700b9caf04f GIT binary patch literal 10244 zcmeHMzi$&U6n-w12sAAPIxsNYgjkST2_eQ%rGG&qv6lR3sL16)+NzAVAi>DS(7h`X zVrSxCVB&vZVnTxN*%shCKj&B?gq|zkOE2HEzwfi-iya~|yQjk)qKt@Is2n%jxI_xS z&$U)o{GEr02Kht^bGZ{(UqV@9YnXd}fm_f6ioN zUUa+tNzn&_KZZYU-Tw3_g&zI|diL}W7VWQSmabR$4(SPvQte8@`%9tLX8#c-lv^30(&hkdNfll*| zbymlZM`aw03ccw`I6+6WO<_c$*pO$&JlmD=$n6LmQ4w-|@G6BB_=;@{oJD-Vn@Z+g zU45k4bLI6#8ZB^7v@ftvWCZ*-Hkp4_>l{xgUb~J%-_O6NumX3nZGpLn4|tP(&TdrZ z%dvgV7W54Kry=*fEpSib1^x?iqMy@bPN<^iRfT0TGRn`KpjvAmhZqN*JZ}% z|9kT9|H~()J*orhz|tHrtKI$XF1qt)>$Q>UW_LK;^=?(nXtsi#?7 + + + com.orangeforms + common + 1.0.0 + + 4.0.0 + + common-core + 1.0.0 + common-core + jar + + + + + com.google.guava + guava + ${guava.version} + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} + + + joda-time + joda-time + ${joda-time.version} + + + org.apache.commons + commons-collections4 + ${commons-collections4.version} + + + org.apache.commons + commons-csv + ${common-csv.version} + + + cn.hutool + hutool-all + ${hutool.version} + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + cn.jimmyshi + bean-query + ${bean.query.version} + + + + org.apache.poi + poi-ooxml + ${poi-ooxml.version} + + + + mysql + mysql-connector-java + 8.0.22 + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + com.sun + jconsole + + + com.sun + tools + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatisplus.version} + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.version} + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java new file mode 100644 index 00000000..8d781115 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyControllerAdvice.java @@ -0,0 +1,31 @@ +package com.orangeforms.common.core.advice; + +import com.orangeforms.common.core.util.MyDateUtil; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Controller的环绕拦截类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@ControllerAdvice +public class MyControllerAdvice { + + /** + * 转换前端传入的日期变量参数为指定格式。 + * + * @param binder 数据绑定参数。 + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.registerCustomEditor(Date.class, + new CustomDateEditor(new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT), false)); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java new file mode 100644 index 00000000..c39771f7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/advice/MyExceptionHandler.java @@ -0,0 +1,167 @@ +package com.orangeforms.common.core.advice; + +import com.orangeforms.common.core.exception.*; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.util.ContextUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.exceptions.PersistenceException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeoutException; + +/** + * 业务层的异常处理类,这里只是给出最通用的Exception的捕捉,今后可以根据业务需要, + * 用不同的函数,处理不同类型的异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@RestControllerAdvice("com.orangeforms") +public class MyExceptionHandler { + + /** + * 通用异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = Exception.class) + public ResponseResult exceptionHandle(Exception ex, HttpServletRequest request) { + log.error("Unhandled exception from URL [" + request.getRequestURI() + "]", ex); + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION, ex.getMessage()); + } + + /** + * 无效的实体对象异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidDataModelException.class) + public ResponseResult invalidDataModelExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidDataModelException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_MODEL); + } + + /** + * 无效的实体对象字段异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidDataFieldException.class) + public ResponseResult invalidDataFieldExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidDataFieldException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD); + } + + /** + * 无效类字段异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidClassFieldException.class) + public ResponseResult invalidClassFieldExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidClassFieldException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_CLASS_FIELD); + } + + /** + * 重复键异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = DuplicateKeyException.class) + public ResponseResult duplicateKeyExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("DuplicateKeyException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY); + } + + /** + * 数据访问失败异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = DataAccessException.class) + public ResponseResult dataAccessExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("DataAccessException exception from URL [" + request.getRequestURI() + "]", ex); + if (ex.getCause() instanceof PersistenceException + && ex.getCause().getCause() instanceof PermissionDeniedDataAccessException) { + return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED); + } + return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED); + } + + /** + * 操作不存在或已逻辑删除数据的异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = NoDataAffectException.class) + public ResponseResult noDataEffectExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("NoDataAffectException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + + /** + * 数据权限异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = NoDataPermException.class) + public ResponseResult noDataPermExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("NoDataPermException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED, ex.getMessage()); + } + + /** + * 自定义运行时异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = MyRuntimeException.class) + public ResponseResult myRuntimeExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("MyRuntimeException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, ex.getMessage()); + } + + /** + * Redis缓存访问异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = RedisCacheAccessException.class) + public ResponseResult redisCacheAccessExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("RedisCacheAccessException exception from URL [" + request.getRequestURI() + "]", ex); + if (ex.getCause() instanceof TimeoutException) { + return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_TIMEOUT); + } + return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_STATE_ERROR); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java new file mode 100644 index 00000000..595e6463 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DeptFilterColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记数据权限中基于DeptId进行过滤的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DeptFilterColumn { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java new file mode 100644 index 00000000..a2f5f028 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableDataFilter.java @@ -0,0 +1,17 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 作为DisableDataFilterAspect的切点。 + * 该注解标记的方法内所有的查询语句,均不会被Mybatis拦截器过滤数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisableDataFilter { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java new file mode 100644 index 00000000..f9a89810 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/DisableTenantFilter.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 仅用于微服务的多租户项目。 + * 用于注解DAO层Mapper对象的租户过滤规则。被包含的方法将不会进行租户Id的过滤。 + * 对于tk mapper和mybatis plus中的内置方法,可以直接指定方法名即可,如:selectOne。 + * 需要说明的是,在大多数场景下,只要在实体对象中指定了租户Id字段,基于该主表的绝大部分增删改操作, + * 都需要经过租户Id过滤,仅当查询非常复杂,或者主表不在SQL语句之中的时候,可以通过该注解禁用该SQL, + * 并根据需求通过手动的方式实现租户过滤。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisableTenantFilter { + + /** + * 包含的方法名称数组。该值不能为空,因为如想取消所有方法的租户过滤, + * 可以通过在实体对象中不指定租户Id字段注解的方式实现。 + * + * @return 被包括的方法名称数组。 + */ + String[] includeMethodName(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java new file mode 100644 index 00000000..cd2f6a36 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/EnableDataPerm.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 用于注解DAO层Mapper对象的数据权限规则。 + * 由于框架使用了tk.mapper,所以并非所有的Mapper接口均在当前Mapper对象中定义,有一部分被tk.mapper封装,如selectAll等。 + * 如果需要排除tk.mapper中的方法,可以直接使用tk.mapper基类所声明的方法名称即可。 + * 另外,比较特殊的场景是,因为tk.mapper是通用框架,所以同样的selectAll方法,可以获取不同的数据集合,因此在service中如果 + * 出现两个不同的方法调用Mapper的selectAll方法,但是一个需要参与过滤,另外一个不需要参与,那么就需要修改当前类的Mapper方法, + * 将其中一个方法重新定义一个具体的接口方法,并重新设定其是否参与数据过滤。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EnableDataPerm { + + /** + * 排除的方法名称数组。如果为空,所有的方法均会被Mybaits拦截注入权限过滤条件。 + * + * @return 被排序的方法名称数据。 + */ + String[] excluseMethodName() default {}; + + /** + * 必须包含能看用户自己数据的数据过滤条件,如果当前用户的数据过滤中,没有DataPermRuleType.TYPE_USER_ONLY, + * 在进行数据权限过滤时,会自动包含该权限。 + * + * @return 是否必须包含DataPermRuleType.TYPE_USER_ONLY类型的数据权限。 + */ + boolean mustIncludeUserRule() default false; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java new file mode 100644 index 00000000..6132c47a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowLatestApprovalStatusColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 业务表中记录流程最后审批状态标记的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlowLatestApprovalStatusColumn { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java new file mode 100644 index 00000000..670a9083 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/FlowStatusColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 业务表中记录流程实例结束标记的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlowStatusColumn { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java new file mode 100644 index 00000000..5546fa00 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/JobUpdateTimeColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记Job实体对象的更新时间字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JobUpdateTimeColumn { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java new file mode 100644 index 00000000..301d5427 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MaskField.java @@ -0,0 +1,50 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.constant.MaskFieldTypeEnum; +import com.orangeforms.common.core.util.MaskFieldHandler; + +import java.lang.annotation.*; + +/** + * 脱敏字段注解。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MaskField { + + /** + * 脱敏类型。 + * + * @return 脱敏类型。 + */ + MaskFieldTypeEnum maskType(); + /** + * 掩码符号。 + * + * @return 掩码符号。 + */ + char maskChar() default '*'; + /** + * 前面noMaskPrefix数量的字符不被掩码。 + * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。 + * + * @return 从1开始计算,前面不被掩码的字符数。 + */ + int noMaskPrefix() default 1; + /** + * 末尾noMaskSuffix数量的字符不被掩码。 + * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。 + * + * @return 从1开始计算,末尾不被掩码的字符数。 + */ + int noMaskSuffix() default 1; + /** + * 自定义脱敏处理器接口的Class。 + * @return 自定义脱敏处理器接口的Class。 + */ + Class handler() default MaskFieldHandler.class; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java new file mode 100644 index 00000000..f12218e7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MultiDatabaseWriteMethod.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 该注解通常标记于Service中的事务方法,并且会和@Transactional注解同时存在。 + * 被注解标注的方法内代码,通常通过mybatis,并在同一个事务内访问数据库。与此同时还会存在基于 + * JDBC的跨库操作。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MultiDatabaseWriteMethod { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java new file mode 100644 index 00000000..6d516240 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSource.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记Service所依赖的数据源类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MyDataSource { + + /** + * 标注的数据源类型 + * @return 当前标注的数据源类型。 + */ + int value(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java new file mode 100644 index 00000000..41b80f8a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyDataSourceResolver.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.util.DataSourceResolver; + +import java.lang.annotation.*; + +/** + * 基于自定义解析规则的多数据源注解。主要用于标注Service的实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MyDataSourceResolver { + + /** + * 多数据源路由键解析接口的Class。 + * @return 多数据源路由键解析接口的Class。 + */ + Class resolver(); + + /** + * DataSourceResolver.resovle方法的入参。 + * @return DataSourceResolver.resovle方法的入参。 + */ + String arg() default ""; + + /** + * 数值型参数。 + * @return DataSourceResolver.resovle方法的入参。 + */ + int intArg() default -1; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java new file mode 100644 index 00000000..4aa12b98 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/MyRequestBody.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标记Controller中的方法参数,参数解析器会根据该注解将请求中的JSON数据,映射到参数中的绑定字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyRequestBody { + + /** + * 是否必须出现的参数。 + */ + boolean required() default false; + /** + * 解析时用到的JSON的key。 + */ + String value() default ""; +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java new file mode 100644 index 00000000..1c832ac2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/NoAuthInterface.java @@ -0,0 +1,15 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记无需Token验证的接口 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NoAuthInterface { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java new file mode 100644 index 00000000..5b695fb0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationConstDict.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标识Model和常量字典之间的关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationConstDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的常量字典的Class对象。 + * + * @return 关联的常量字典的Class对象。 + */ + Class constantDictClass(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java new file mode 100644 index 00000000..7b592496 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationDict.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的字典关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联Model对象的关联Name字段名称。 + * + * @return 被关联Model对象的关联Name字段名称。 + */ + String slaveNameField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 在同一个实体对象中,如果有一对一关联和字典关联,都是基于相同的主表字段,并关联到 + * 相同关联表的同一关联字段时,可以在字典关联的注解中引用被一对一注解标准的对象属性。 + * 从而在数据整合时,当前字典的数据可以直接取自"equalOneToOneRelationField"指定 + * 的字段,从而避免一次没必要的数据库查询操作,提升了加载显示的效率。 + * + * @return 与该字典字段引用关系完全相同的一对一关联属性名称。 + */ + String equalOneToOneRelationField() default ""; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java new file mode 100644 index 00000000..65ab2a5a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationGlobalDict.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 全局字典关联。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationGlobalDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 全局字典编码。 + * + * @return 全局字典编码。空表示为不使用全局字典。 + */ + String dictCode(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java new file mode 100644 index 00000000..bee48192 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToMany.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标注多对多的Model关系。 + * 重要提示:由于多对多关联表数据,很多时候都不需要跟随主表数据返回,所以该注解不会在 + * 生成的时候自动添加到实体类字段上,需要的时候,用户可自行手动添加。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationManyToMany { + + /** + * 多对多中间表的Mapper对象名称。 + * 如果是空字符串,BaseService会自动拼接为 relationModelClass().getSimpleName() + "Mapper"。 + * + * @return 被关联的本地Service对象名称。 + */ + String relationMapperName() default ""; + + /** + * 多对多关联表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class relationModelClass(); + + /** + * 多对多关联表Model对象中与主表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationMasterIdField(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java new file mode 100644 index 00000000..cfa48e2f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationManyToManyAggregation.java @@ -0,0 +1,96 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 主要用于多对多的Model关系。标注通过从表关联字段或者关联表关联字段计算主表聚合计算字段的规则。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationManyToManyAggregation { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 多对多从表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 多对多从表Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 多对多关联表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class relationModelClass(); + + /** + * 多对多关联表Model对象中与主表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationMasterIdField(); + + /** + * 多对多关联表Model对象中与从表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationSlaveIdField(); + + /** + * 聚合计算所在的Model。 + * + * @return 聚合计算所在Model的Class。 + */ + Class aggregationModelClass(); + + /** + * 聚合类型。具体数值参考AggregationType对象。 + * + * @return 聚合类型。 + */ + int aggregationType(); + + /** + * 聚合计算所在Model的字段名称。 + * + * @return 聚合计算所在Model的字段名称。 + */ + String aggregationField(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java new file mode 100644 index 00000000..5a5d6e16 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToMany.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的一对多关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToMany { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java new file mode 100644 index 00000000..61befd73 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToManyAggregation.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 主要用于一对多的Model关系。标注通过从表关联字段计算主表聚合计算字段的规则。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToManyAggregation { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联Model对象中参与计算的聚合类型。具体数值参考AggregationType对象。 + * + * @return 被关联Model对象中参与计算的聚合类型。 + */ + int aggregationType(); + + /** + * 被关联Model对象中参与聚合计算的字段名称。 + * + * @return 被关联Model对象中参与计算字段的名称。 + */ + String aggregationField(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java new file mode 100644 index 00000000..fd38ca49 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/RelationOneToOne.java @@ -0,0 +1,61 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的一对一关联关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToOne { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级低于 slaveServiceClass(), + * 如果是空字符串,BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 在一对一关联时,是否加载从表的字典关联。 + * + * @return 是否加载从表的字典关联。true关联,false则只返回从表自身数据。 + */ + boolean loadSlaveDict() default true; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java new file mode 100644 index 00000000..368a9ea2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/TenantFilterColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记通过租户Id进行过滤的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantFilterColumn { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java new file mode 100644 index 00000000..c01e6a16 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UploadFlagColumn.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.annotation; + +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; + +import java.lang.annotation.*; + +/** + * 用于标记支持数据上传和下载的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UploadFlagColumn { + + /** + * 上传数据存储类型。 + * + * @return 上传数据存储类型。 + */ + UploadStoreTypeEnum storeType(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java new file mode 100644 index 00000000..af9275e2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/annotation/UserFilterColumn.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记数据权限中基于UserId进行过滤的字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UserFilterColumn { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java new file mode 100644 index 00000000..5acff1a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceAspect.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.core.aop; + +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.config.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +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.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 多数据源AOP切面处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DataSourceAspect { + + /** + * 所有配置MyDataSource注解的Service实现类。 + */ + @Pointcut("execution(public * com.orangeforms..service..*(..)) " + + "&& @target(com.orangeforms.common.core.annotation.MyDataSource)") + public void datasourcePointCut() { + // 空注释,避免sonar警告 + } + + @Around("datasourcePointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + Class clazz = point.getTarget().getClass(); + MyDataSource ds = clazz.getAnnotation(MyDataSource.class); + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 + Integer originalType = DataSourceContextHolder.setDataSourceType(ds.value()); + log.debug("set datasource is " + ds.value()); + try { + return point.proceed(); + } finally { + DataSourceContextHolder.unset(originalType); + log.debug("unset datasource is " + originalType); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java new file mode 100644 index 00000000..f2697a64 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/aop/DataSourceResolveAspect.java @@ -0,0 +1,73 @@ +package com.orangeforms.common.core.aop; + +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.util.DataSourceResolver; +import com.orangeforms.common.core.config.DataSourceContextHolder; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +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.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 基于自定义解析规则的多数据源AOP切面处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DataSourceResolveAspect { + + private final Map, DataSourceResolver> resolverMap = new ConcurrentHashMap<>(); + + /** + * 所有配置MyDataSourceResovler注解的Service实现类。 + */ + @Pointcut("execution(public * com.orangeforms..service..*(..)) " + + "&& @target(com.orangeforms.common.core.annotation.MyDataSourceResolver)") + public void datasourceResolverPointCut() { + // 空注释,避免sonar警告 + } + + @Around("datasourceResolverPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + Class clazz = point.getTarget().getClass(); + MyDataSourceResolver dsr = clazz.getAnnotation(MyDataSourceResolver.class); + Class resolverClass = dsr.resolver(); + DataSourceResolver resolver = + resolverMap.computeIfAbsent(resolverClass, ApplicationContextHolder::getBean); + Integer type = resolver.resolve(dsr.arg(), dsr.intArg(), this.getMethodName(point), point.getArgs()); + Integer originalType = null; + if (type != null) { + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 + originalType = DataSourceContextHolder.setDataSourceType(type); + log.debug("set datasource is " + type); + } + try { + return point.proceed(); + } finally { + if (type != null) { + DataSourceContextHolder.unset(originalType); + log.debug("unset datasource is " + originalType); + } + } + } + + private String getMethodName(JoinPoint joinPoint) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + return methodSignature.getMethod().getName(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java new file mode 100644 index 00000000..8da1978e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/dao/BaseDaoMapper.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.core.base.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +/** + * 数据访问对象的基类。 + * + * @param 主Model实体对象。 + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseDaoMapper extends BaseMapper { + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy 分组字段列表,逗号分隔。 + * @return 对象可选字段Map列表。 + */ + @Select("") + List> getGroupedListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("groupBy") String groupBy); + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和排序字符串,返回查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 选择的字段列表。 + * @param whereClause 过滤字符串。 + * @param orderBy 排序字符串。 + * @return 查询结果。 + */ + @Select("") + List> getListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("orderBy") String orderBy); + + /** + * 用指定过滤条件,计算记录数量。 + * + * @param selectTable 表名称。 + * @param whereClause 过滤字符串。 + * @return 返回过滤后的数据数量。 + */ + @Select("") + int getCountByCondition(@Param("selectTable") String selectTable, @Param("whereClause") String whereClause); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java new file mode 100644 index 00000000..0713d5e4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/BaseModelMapper.java @@ -0,0 +1,124 @@ +package com.orangeforms.common.core.base.mapper; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Model对象到Domain类型对象的相互转换。实现类通常声明在Model实体类中。 + * + * @param Domain域对象类型。 + * @param Model实体对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseModelMapper { + + /** + * 转换Model实体对象到Domain域对象。 + * + * @param model Model实体对象。 + * @return Domain域对象。 + */ + D fromModel(M model); + + /** + * 转换Model实体对象列表到Domain域对象列表。 + * + * @param modelList Model实体对象列表。 + * @return Domain域对象列表。 + */ + List fromModelList(List modelList); + + /** + * 转换Domain域对象到Model实体对象。 + * + * @param domain Domain域对象。 + * @return Model实体对象。 + */ + M toModel(D domain); + + /** + * 转换Domain域对象列表到Model实体对象列表。 + * + * @param domainList Domain域对象列表。 + * @return Model实体对象列表。 + */ + List toModelList(List domainList); + + /** + * 转换bean到map + * + * @param bean bean对象。 + * @param ignoreNullValue 值为null的字段是否转换到Map。 + * @param bean类型。 + * @return 转换后的map对象。 + */ + default Map beanToMap(T bean, boolean ignoreNullValue) { + return BeanUtil.beanToMap(bean, false, ignoreNullValue); + } + + /** + * 转换bean集合到map集合 + * + * @param dataList bean对象集合。 + * @param ignoreNullValue 值为null的字段是否转换到Map。 + * @param bean类型。 + * @return 转换后的map对象集合。 + */ + default List> beanToMap(List dataList, boolean ignoreNullValue) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream() + .map(o -> BeanUtil.beanToMap(o, false, ignoreNullValue)) + .collect(Collectors.toList()); + } + + /** + * 转换map到bean。 + * + * @param map map对象。 + * @param beanClazz bean的Class对象。 + * @param bean类型。 + * @return 转换后的bean对象。 + */ + default T mapToBean(Map map, Class beanClazz) { + return BeanUtil.toBeanIgnoreError(map, beanClazz); + } + + /** + * 转换map集合到bean集合。 + * + * @param mapList map对象集合。 + * @param beanClazz bean的Class对象。 + * @param bean类型。 + * @return 转换后的bean对象集合。 + */ + default List mapToBean(List> mapList, Class beanClazz) { + if (CollUtil.isEmpty(mapList)) { + return new LinkedList<>(); + } + return mapList.stream() + .map(m -> BeanUtil.toBeanIgnoreError(m, beanClazz)) + .collect(Collectors.toList()); + } + + /** + * 对于Map字段到Map字段的映射场景,MapStruct会根据方法签名自动选择该函数 + * 作为对象copy的函数。由于该函数是直接返回的,因此没有对象copy,效率更高。 + * 如果没有该函数,MapStruct会生成如下代码: + * Map map = courseDto.getTeacherIdDictMap(); + * if ( map != null ) { + * course.setTeacherIdDictMap( new HashMap( map ) ); + * } + * + * @param map map对象。 + * @return 直接返回的map。 + */ + default Map mapToMap(Map map) { + return map; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java new file mode 100644 index 00000000..3052c396 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/mapper/DummyModelMapper.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.core.base.mapper; + +import java.util.List; + +/** + * 哑元占位对象。Model实体对象和Domain域对象相同的场景下使用。 + * 由于没有实际的数据转换,因此同时保证了代码统一和执行效率。 + * + * @param 数据类型。 + * @author Jerry + * @date 2024-07-02 + */ +public class DummyModelMapper implements BaseModelMapper { + + /** + * 不转换直接返回。 + * + * @param model Model实体对象。 + * @return Domain域对象。 + */ + @Override + public M fromModel(M model) { + return model; + } + + /** + * 不转换直接返回。 + * + * @param modelList Model实体对象列表。 + * @return Domain域对象列表。 + */ + @Override + public List fromModelList(List modelList) { + return modelList; + } + + /** + * 不转换直接返回。 + * + * @param domain Domain域对象。 + * @return Model实体对象。 + */ + @Override + public M toModel(M domain) { + return domain; + } + + /** + * 不转换直接返回。 + * + * @param domainList Domain域对象列表。 + * @return Model实体对象列表。 + */ + @Override + public List toModelList(List domainList) { + return domainList; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java new file mode 100644 index 00000000..6421b726 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/model/BaseModel.java @@ -0,0 +1,40 @@ +package com.orangeforms.common.core.base.model; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.util.Date; + +/** + * 实体对象的公共基类,所有子类均必须包含基类定义的数据表字段和实体对象字段。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class BaseModel { + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java new file mode 100644 index 00000000..d10aa029 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseDictService.java @@ -0,0 +1,229 @@ +package com.orangeforms.common.core.base.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.cache.DictionaryCache; +import com.orangeforms.common.core.object.TokenData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.*; + +/** + * 带有缓存功能的字典Service基类,需要留意的是,由于缓存基于Key/Value方式存储, + * 目前仅支持基于主键字段的缓存查找,其他条件的查找仍然从数据源获取。 + * + * @param Model实体对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class BaseDictService + extends BaseService implements IBaseDictService { + + /** + * 缓存池对象。 + */ + protected DictionaryCache dictionaryCache; + + /** + * 构造函数使用缺省缓存池对象。 + */ + protected BaseDictService() { + super(); + } + + /** + * 重新加载数据库中所有当前表数据到系统内存。 + * + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + @Override + public void reloadCachedData(boolean force) { + // 在非强制刷新情况下。 + // 先行判断缓存中是否存在数据,如果有就不加载了。 + if (!force && dictionaryCache.getCount() > 0) { + return; + } + List allList = super.getAllList(); + dictionaryCache.reload(allList, force); + } + + /** + * 保存新增对象。 + * + * @param data 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public M saveNew(M data) { + // 清空全部缓存 + dictionaryCache.invalidateAll(); + if (deletedFlagFieldName != null) { + ReflectUtil.setFieldValue(data, deletedFlagFieldName, GlobalDeletedFlag.NORMAL); + } + if (tenantIdField != null) { + ReflectUtil.setFieldValue(data, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + mapper().insert(data); + return data; + } + + /** + * 更新数据对象。 + * + * @param data 更新的对象。 + * @param originalData 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(M data, M originalData) { + dictionaryCache.invalidateAll(); + if (tenantIdField != null) { + ReflectUtil.setFieldValue(data, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + return mapper().updateById(data) == 1; + } + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(K id) { + dictionaryCache.invalidateAll(); + return mapper().deleteById(id) == 1; + } + + /** + * 直接从缓存池中获取主键Id关联的数据。如果缓存中不存在,再从数据库中取出并回写到缓存。 + * + * @param id 主键Id。 + * @return 主键关联的数据,不存在返回null。 + */ + @SuppressWarnings("unchecked") + @Override + public M getById(Serializable id) { + M data = dictionaryCache.get((K) id); + if (data != null) { + return data; + } + if (dictionaryCache.getCount() != 0) { + return data; + } + this.reloadCachedData(true); + return dictionaryCache.get((K) id); + } + + /** + * 直接从缓存池中获取所有数据。 + * + * @return 返回所有数据。 + */ + @Override + public List getAllListFromCache() { + List resultList = dictionaryCache.getAll(); + if (CollUtil.isNotEmpty(resultList)) { + return resultList; + } + this.reloadCachedData(true); + return dictionaryCache.getAll(); + } + + /** + * 直接从缓存池中返回符合主键 in (idValues) 条件的所有数据。 + * 对于缓存中不存在的数据,从数据库中获取并回写入缓存。 + * + * @param idValues 主键值列表。 + * @return 检索后的数据列表。 + */ + @Override + public List getInList(Set idValues) { + List resultList = dictionaryCache.getInList(idValues); + // 如果从缓存中获取与请求的id完全相同就直接返回。 + if (resultList.size() == idValues.size()) { + return resultList; + } + // 如果此时缓存中存在数据,说明有部分id是不存在的。也可以直接返回了。 + if (dictionaryCache.getCount() != 0) { + return resultList; + } + // 执行到这里,说明缓存是空的,所有需要重新加载并再次从缓存中读取并返回。 + this.reloadCachedData(true); + return dictionaryCache.getInList(idValues); + } + + @Override + public List getListByParentId(K parentId) { + List resultList = dictionaryCache.getListByParentId(parentId); + // 如果包含数据就直接返回了 + if (CollUtil.isNotEmpty(resultList)) { + return resultList; + } + // 如果缓存中存在该字典数据,说明该parentId下子对象列表为空,也可以直接返回了。 + if (this.getCachedCount() != 0) { + return resultList; + } + // 执行到这里就需要重新加载全部缓存了。 + this.reloadCachedData(true); + return dictionaryCache.getListByParentId(parentId); + } + + /** + * 返回符合 inFilterField in (inFilterValues) 条件的所有数据。属性property是主键,则从缓存中读取。 + * + * @param inFilterField 参与(In-list)过滤的Java字段。 + * @param inFilterValues 参与(In-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + @SuppressWarnings("unchecked") + @Override + public List getInList(String inFilterField, Set inFilterValues) { + if (inFilterField.equals(this.idFieldName)) { + return this.getInList((Set) inFilterValues); + } + return super.getInList(inFilterField, inFilterValues); + } + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id。 + * @param inFilterValues 数据值集合。 + * @return 全部存在返回true,否则false。 + */ + @SuppressWarnings("unchecked") + @Override + public boolean existUniqueKeyList(String inFilterField, Set inFilterValues) { + if (CollUtil.isEmpty(inFilterValues)) { + return true; + } + if (inFilterField.equals(this.idFieldName)) { + List dataList = this.getInList((Set) inFilterValues); + return dataList.size() == inFilterValues.size(); + } + String columnName = this.safeMapToColumnName(inFilterField); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(columnName, inFilterValues); + return mapper().selectCount(queryWrapper) == inFilterValues.size(); + } + + /** + * 获取缓存中的数据数量。 + * + * @return 缓存中的数据总量。 + */ + @Override + public int getCachedCount() { + return dictionaryCache.getCount(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java new file mode 100644 index 00000000..ef6c5a7d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/BaseService.java @@ -0,0 +1,2368 @@ +package com.orangeforms.common.core.base.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ReflectUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.constant.AggregationType; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.constant.MaskFieldTypeEnum; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.lang.reflect.Modifier; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import static java.util.stream.Collectors.*; + +/** + * 所有Service的基类。 + * + * @param Model对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class BaseService extends ServiceImpl, M> implements IBaseService { + + /** + * 当前Service关联的主Model实体对象的Class。 + */ + protected final Class modelClass; + /** + * 当前Service关联的主Model实体对象主键字段的Class。 + */ + protected final Class idFieldClass; + /** + * 当前Service关联的主Model实体对象的实际表名称。 + */ + protected final String tableName; + /** + * 当前Service关联的主Model对象主键字段名称。 + */ + protected String idFieldName; + /** + * 当前Service关联的主数据表中主键列名称。 + */ + protected String idColumnName; + /** + * 当前Service关联的主Model对象逻辑删除字段名称。 + */ + protected String deletedFlagFieldName; + /** + * 当前Service关联的主数据表中逻辑删除字段名称。 + */ + protected String deletedFlagColumnName; + /** + * 当前Service关联的主Model对象租户Id字段。 + */ + protected Field tenantIdField; + /** + * 流程实例状态字段。 + */ + protected Field flowStatusField; + /** + * 流程最后审批状态字段 + */ + protected Field flowLatestApprovalStatusField; + /** + * 脱敏字段列表。 + */ + protected List maskFieldList; + /** + * 当前Service关联的主Model对象租户Id字段名称。 + */ + protected String tenantIdFieldName; + /** + * 当前Service关联的主数据表中租户Id列名称。 + */ + protected String tenantIdColumnName; + /** + * 当前Job服务源主表Model对象最后更新时间字段名称。 + */ + protected String jobUpdateTimeFieldName; + /** + * 当前Job服务源主表Model对象最后更新时间列名称。 + */ + protected String jobUpdateTimeColumnName; + /** + * 当前业务服务源主表Model对象最后更新时间字段名称。 + */ + protected String updateTimeFieldName; + /** + * 当前业务服务源主表Model对象最后更新时间列名称。 + */ + protected String updateTimeColumnName; + /** + * 当前业务服务源主表Model对象最后更新用户Id字段名称。 + */ + protected String updateUserIdFieldName; + /** + * 当前业务服务源主表Model对象最后更新用户Id列名称。 + */ + protected String updateUserIdColumnName; + /** + * 当前Service关联的主Model对象主键字段赋值方法的反射对象。 + */ + protected Method setIdFieldMethod; + /** + * 当前Service关联的主Model对象主键字段访问方法的反射对象。 + */ + protected Method getIdFieldMethod; + /** + * 当前Service关联的主Model对象逻辑删除字段赋值方法的反射对象。 + */ + protected Method setDeletedFlagMethod; + /** + * 当前Service关联的全局字典对象的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List relationGlobalDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有常量字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List relationConstDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有一对一关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToOneStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有一对多关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToManyStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有多对多关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationManyToManyStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有一对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToManyAggrStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有多对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationManyToManyAggrStructList = new LinkedList<>(); + /** + * 基础表的实体对象及表信息。 + */ + protected final TableModelInfo tableModelInfo = new TableModelInfo(); + private final Map, MaskFieldHandler> maskFieldHandlerMap = new ConcurrentHashMap<>(); + + private static final String GROUPED_KEY = "GROUPED_KEY"; + private static final String AGGREGATED_VALUE = "AGGREGATED_VALUE"; + private static final String AND_OP = " AND "; + private static final String ORDER_BY = " ORDER BY "; + + @Override + public BaseDaoMapper getBaseMapper() { + return mapper(); + } + + /** + * 构造函数,在实例化的时候,一次性完成所有有关主Model对象信息的加载。 + */ + @SuppressWarnings("unchecked") + protected BaseService() { + Class type = getClass(); + while (!(type.getGenericSuperclass() instanceof ParameterizedType)) { + type = type.getSuperclass(); + } + modelClass = (Class) ((ParameterizedType) type.getGenericSuperclass()).getActualTypeArguments()[0]; + idFieldClass = (Class) ((ParameterizedType) type.getGenericSuperclass()).getActualTypeArguments()[1]; + this.tableName = modelClass.getAnnotation(TableName.class).value(); + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field field : fields) { + initializeField(field); + } + tableModelInfo.setModelName(modelClass.getSimpleName()); + tableModelInfo.setTableName(this.tableName); + tableModelInfo.setKeyFieldName(idFieldName); + tableModelInfo.setKeyColumnName(idColumnName); + } + + @Override + public TableModelInfo getTableModelInfo() { + return this.tableModelInfo; + } + + private void initializeField(Field field) { + if (idFieldName == null && null != field.getAnnotation(TableId.class)) { + idFieldName = field.getName(); + TableId c = field.getAnnotation(TableId.class); + idColumnName = c == null ? idFieldName : c.value(); + setIdFieldMethod = ReflectUtil.getMethod( + modelClass, "set" + StrUtil.upperFirst(idFieldName), idFieldClass); + getIdFieldMethod = ReflectUtil.getMethod( + modelClass, "get" + StrUtil.upperFirst(idFieldName)); + } + if (null != field.getAnnotation(JobUpdateTimeColumn.class)) { + jobUpdateTimeFieldName = field.getName(); + jobUpdateTimeColumnName = this.safeMapToColumnName(jobUpdateTimeFieldName); + } + if (null != field.getAnnotation(TableLogic.class)) { + deletedFlagFieldName = field.getName(); + deletedFlagColumnName = this.safeMapToColumnName(deletedFlagFieldName); + setDeletedFlagMethod = ReflectUtil.getMethod( + modelClass, "set" + StrUtil.upperFirst(deletedFlagFieldName), Integer.class); + } + if (null != field.getAnnotation(TenantFilterColumn.class)) { + tenantIdField = field; + tenantIdFieldName = field.getName(); + tenantIdColumnName = this.safeMapToColumnName(tenantIdFieldName); + } + if (null != field.getAnnotation(FlowStatusColumn.class)) { + flowStatusField = field; + } + if (null != field.getAnnotation(FlowLatestApprovalStatusColumn.class)) { + flowLatestApprovalStatusField = field; + } + if (null != field.getAnnotation(MaskField.class)) { + if (maskFieldList == null) { + maskFieldList = new LinkedList<>(); + } + maskFieldList.add(field); + } + } + + /** + * 获取子类中注入的Mapper类。 + * + * @return 子类中注入的Mapper类。 + */ + protected abstract BaseDaoMapper mapper(); + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveBatch(Collection dataList) { + dataList.forEach(baseMapper::insert); + return true; + } + + @SuppressWarnings("unchecked") + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewOrUpdate(M data, Consumer saveNew, BiConsumer update) { + if (data == null) { + return; + } + K id = (K) ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + saveNew.accept(data); + } else { + update.accept(data, this.getById(id)); + } + } + + @SuppressWarnings("unchecked") + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewOrUpdateBatch(List dataList, Consumer> saveNewBatch, BiConsumer update) { + if (CollUtil.isEmpty(dataList)) { + return; + } + List saveNewDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) == null).collect(toList()); + if (CollUtil.isNotEmpty(saveNewDataList)) { + saveNewBatch.accept(saveNewDataList); + } + List updateDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null).collect(toList()); + if (CollUtil.isNotEmpty(updateDataList)) { + for (M data : updateDataList) { + K id = (K) ReflectUtil.getFieldValue(data, idFieldName); + update.accept(data, this.getById(id)); + } + } + } + + /** + * 根据过滤条件删除数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Integer removeBy(M filter) { + return mapper().delete(new QueryWrapper<>(filter)); + } + + @Transactional(rollbackFor = Exception.class) + public boolean remove(K id) { + return mapper().deleteById(id) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateBatchOneToManyRelation( + String relationFieldName, + Object relationFieldValue, + String updateUserIdFieldName, + String updateTimeFieldName, + List dataList, + Consumer> batchInserter) { + // 删除在现有数据列表dataList中不存在的从表数据。 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(this.safeMapToColumnName(relationFieldName), relationFieldValue); + if (CollUtil.isNotEmpty(dataList)) { + Set keptIdSet = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + if (CollUtil.isNotEmpty(keptIdSet)) { + queryWrapper.notIn(idColumnName, keptIdSet); + } + } + mapper().delete(queryWrapper); + if (CollUtil.isEmpty(dataList)) { + return; + } + // 没有包含主键的对象被视为新对象,为了效率最优化,这里执行批量插入。 + List newDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) == null).collect(toList()); + if (CollUtil.isNotEmpty(newDataList)) { + newDataList.forEach(o -> ReflectUtil.setFieldValue(o, relationFieldName, relationFieldValue)); + batchInserter.accept(newDataList); + } + // 对于主键已经存在的数据,我们视为已存在数据,这里执行逐条更新操作。 + List updateDataList = + dataList.stream().filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null).toList(); + for (M updateData : updateDataList) { + // 如果前端将更新用户Id置空,这里使用当前用户更新该字段。 + if (updateUserIdFieldName != null) { + ReflectUtil.setFieldValue(updateData, updateUserIdFieldName, TokenData.takeFromRequest().getUserId()); + } + // 如果前端将更新时间置空,这里使用当前时间更新该字段。 + if (updateTimeFieldName != null) { + ReflectUtil.setFieldValue(updateData, updateTimeFieldName, new Date()); + } + if (this.tenantIdField != null) { + ReflectUtil.setFieldValue(updateData, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + if (this.deletedFlagFieldName != null) { + ReflectUtil.setFieldValue(updateData, deletedFlagFieldName, GlobalDeletedFlag.NORMAL); + } + @SuppressWarnings("unchecked") + K id = (K) ReflectUtil.getFieldValue(updateData, idFieldName); + this.compareAndSetMaskFieldData(updateData, id); + mapper().updateById(updateData); + } + } + + /** + * 判断指定字段的数据是否存在,且仅仅存在一条记录。 + * 如果是基于主键的过滤,会直接调用existId过滤函数,提升性能。在有缓存的场景下,也可以利用缓存。 + * + * @param fieldName 待过滤的字段名(Java 字段)。 + * @param fieldValue 字段值。 + * @return 存在且仅存在一条返回true,否则false。 + */ + @SuppressWarnings("unchecked") + @Override + public boolean existOne(String fieldName, Object fieldValue) { + if (fieldName.equals(this.idFieldName)) { + return this.existId((K) fieldValue); + } + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + return mapper().selectCount(new QueryWrapper().eq(columnName, fieldValue)) == 1; + } + + /** + * 判断主键Id关联的数据是否存在。 + * + * @param id 主键Id。 + * @return 存在返回true,否则false。 + */ + @Override + public boolean existId(K id) { + return getById(id) != null; + } + + @Override + public M getOne(M filter) { + return mapper().selectOne(new QueryWrapper<>(filter)); + } + + /** + * 返回符合 filterField = filterValue 条件的一条数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterValue 过滤的Java字段值。 + * @return 查询后的数据对象。 + */ + @SuppressWarnings("unchecked") + @Override + public M getOne(String filterField, Object filterValue) { + if (filterField.equals(idFieldName)) { + return this.getById((K) filterValue); + } + String columnName = this.safeMapToColumnName(filterField); + QueryWrapper queryWrapper = new QueryWrapper().eq(columnName, filterValue); + return mapper().selectOne(queryWrapper); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param id 主表主键Id。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 查询结果对象。 + */ + @Override + public M getByIdWithRelation(K id, MyRelationParam relationParam) { + M dataObject = this.getById(id); + this.buildRelationForData(dataObject, relationParam); + return dataObject; + } + + @Override + public M getById(Serializable id) { + return this.mapper().selectById(id); + } + + /** + * 获取所有数据。 + * + * @return 返回所有数据。 + */ + @Override + public List getAllList() { + return mapper().selectList(Wrappers.emptyWrapper()); + } + + /** + * 获取排序后所有数据。 + * + * @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 + * @return 返回排序后所有数据。 + */ + @Override + public List getAllListByOrder(String... orderByProperties) { + List columns = new ArrayList<>(orderByProperties.length); + for (String orderByProperty : orderByProperties) { + columns.add(this.safeMapToColumnName(orderByProperty)); + } + return mapper().selectList(new QueryWrapper().orderByAsc(columns)); + } + + /** + * 判断参数值主键集合中的所有数据,是否全部存在 + * + * @param idSet 待校验的主键集合。 + * @return 全部存在返回true,否则false。 + */ + @Override + public boolean existAllPrimaryKeys(Set idSet) { + if (CollUtil.isEmpty(idSet)) { + return true; + } + return this.existUniqueKeyList(idFieldName, idSet); + } + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id + * @param inFilterValues 数据值列表。 + * @return 全部存在返回true,否则false。 + */ + @Override + public boolean existUniqueKeyList(String inFilterField, Set inFilterValues) { + if (CollUtil.isEmpty(inFilterValues)) { + return true; + } + String column = this.safeMapToColumnName(inFilterField); + return mapper().selectCount(new QueryWrapper().in(column, inFilterValues)) == inFilterValues.size(); + } + + @Override + public List notExist(String filterField, Set filterSet, boolean findFirst) { + List notExistIdList = new LinkedList<>(); + int start = 0; + int count = 1000; + if (filterSet.size() > count) { + do { + int end = Math.min(filterSet.size(), start + count); + List subFilterList = CollUtil.sub(filterSet, start, end); + doNotExistQuery(filterField, subFilterList, findFirst, notExistIdList); + if ((findFirst && CollUtil.isNotEmpty(notExistIdList)) || end == filterSet.size()) { + break; + } + start += count; + } while (true); + } else { + doNotExistQuery(filterField, filterSet, findFirst, notExistIdList); + } + return notExistIdList; + } + + private void doNotExistQuery( + String filterField, Collection filterSet, boolean findFirst, List notExistIdList) { + String columnName = this.safeMapToColumnName(filterField); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(columnName, filterSet); + queryWrapper.select(columnName); + Set existIdSet = mapper().selectList(queryWrapper).stream() + .map(c -> ReflectUtil.getFieldValue(c, filterField)).collect(toSet()); + for (R filterData : filterSet) { + if (!existIdSet.contains(filterData)) { + notExistIdList.add(filterData); + if (findFirst) { + break; + } + } + } + } + + @Override + public List getInList(Set idValues) { + return this.getInList(idFieldName, idValues, null); + } + + @Override + public List getInList(String inFilterField, Set inFilterValues) { + return this.getInList(inFilterField, inFilterValues, null); + } + + @Override + public List getInList(String inFilterField, Set inFilterValues, String orderBy) { + if (CollUtil.isEmpty(inFilterValues)) { + return new LinkedList<>(); + } + String column = this.safeMapToColumnName(inFilterField); + QueryWrapper queryWrapper = new QueryWrapper().in(column, inFilterValues); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + return mapper().selectList(queryWrapper); + } + + @Override + public List getInListWithRelation(Set idValues, MyRelationParam relationParam) { + List resultList = this.getInList(idValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam) { + List resultList = this.getInList(inFilterField, inFilterValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam) { + List resultList = this.getInList(inFilterField, inFilterValues, orderBy); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInList(Set idValues) { + return this.getNotInList(idFieldName, idValues, null); + } + + @Override + public List getNotInList(String inFilterField, Set inFilterValues) { + return this.getNotInList(inFilterField, inFilterValues, null); + } + + @Override + public List getNotInList(String inFilterField, Set inFilterValues, String orderBy) { + QueryWrapper queryWrapper; + if (CollUtil.isEmpty(inFilterValues)) { + queryWrapper = new QueryWrapper<>(); + } else { + String column = this.safeMapToColumnName(inFilterField); + queryWrapper = new QueryWrapper().notIn(column, inFilterValues); + } + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + return mapper().selectList(queryWrapper); + } + + @Override + public List getNotInListWithRelation(Set idValues, MyRelationParam relationParam) { + List resultList = this.getNotInList(idValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInListWithRelation( + String inFilterField, Set inFilterValues, MyRelationParam relationParam) { + List resultList = this.getNotInList(inFilterField, inFilterValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam) { + List resultList = this.getNotInList(inFilterField, inFilterValues, orderBy); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public long getCountByFilter(M filter) { + return mapper().selectCount(new QueryWrapper<>(filter)); + } + + @Override + public boolean existByFilter(M filter) { + return this.getCountByFilter(filter) > 0; + } + + @Override + public List getListByFilter(M filter) { + return mapper().selectList(new QueryWrapper<>(filter)); + } + + @Override + public List getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + List resultList = mapper().selectList(queryWrapper); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + /** + * 获取父主键Id下的所有子数据列表。 + * + * @param parentIdFieldName 父主键字段名字,如"courseId"。 + * @param parentId 父主键的值。 + * @return 父主键Id下的所有子数据列表。 + */ + @Override + public List getListByParentId(String parentIdFieldName, K parentId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + String parentIdColumn = this.safeMapToColumnName(parentIdFieldName); + if (parentId != null) { + queryWrapper.eq(parentIdColumn, parentId); + } else { + queryWrapper.isNull(parentIdColumn); + } + return mapper().selectList(queryWrapper); + } + + /** + * 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectFields 选择的字段列表,多个字段逗号分隔。 + * NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 + * 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy SQL常量形式分组字段列表,逗号分隔。 + * @return 聚合计算后的数据结果集。 + */ + @Override + public List> getGroupedListByCondition( + String selectFields, String whereClause, String groupBy) { + return mapper().getGroupedListByCondition(tableName, selectFields, whereClause, groupBy); + } + + /** + * 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 + * @param filter 过滤对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param orderBy SQL常量形式排序字段列表,逗号分隔。 + * @return 查询结果。 + */ + @Override + public List getListByCondition(List selectList, M filter, String whereClause, String orderBy) { + QueryWrapper queryWrapper = new QueryWrapper<>(filter); + if (CollUtil.isNotEmpty(selectList)) { + String[] columns = new String[selectList.size()]; + for (int i = 0; i < selectList.size(); i++) { + columns[i] = this.safeMapToColumnName(selectList.get(i)); + } + queryWrapper.select(columns); + } + if (StrUtil.isNotBlank(whereClause)) { + queryWrapper.apply(whereClause); + } + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + return mapper().selectList(queryWrapper); + } + + /** + * 用指定过滤条件,计算记录数量。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param whereClause SQL常量形式的条件从句。 + * @return 返回过滤后的数据数量。 + */ + @Override + public Integer getCountByCondition(String whereClause) { + return mapper().getCountByCondition(this.tableName, whereClause); + } + + @Override + public void maskFieldData(M data, Set ignoreFieldSet) { + if (data != null) { + this.maskFieldDataList(CollUtil.newArrayList(data), ignoreFieldSet); + } + } + + @Override + public void maskFieldDataList(List dataList, Set ignoreFieldSet) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field maskField : maskFieldList) { + if (!CollUtil.contains(ignoreFieldSet, maskField.getName())) { + MaskField anno = maskField.getAnnotation(MaskField.class); + for (M data : dataList) { + Object maskedValue = this.doMaskFieldData(data, maskField, anno); + ReflectUtil.setFieldValue(data, maskField, maskedValue); + } + } + } + } + + @Override + public void compareAndSetMaskFieldData(M data, M originalData) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field maskField : maskFieldList) { + Object value = ReflectUtil.getFieldValue(data, maskField); + if (value == null) { + continue; + } + MaskField anno = maskField.getAnnotation(MaskField.class); + String maskChar = String.valueOf(anno.maskChar()); + // 如果此时包含了掩码字符,说明数据没有变化,就要和原字段值脱敏后的结果比对。 + // 如果一致就用脱敏前的原值,覆盖当前提交的(包含掩码的)值,否则说明进行了部分 + // 修改,但是字段值中仍然含有掩码字符,这是不允许的。 + if (value.toString().contains(maskChar)) { + Object maskedOriginalValue = this.doMaskFieldData(originalData, maskField, anno); + if (ObjectUtil.notEqual(value, maskedOriginalValue)) { + throw new MyRuntimeException("数据验证失败,不能仅修改部分脱敏数据!"); + } + Object originalValue = ReflectUtil.getFieldValue(originalData, maskField); + ReflectUtil.setFieldValue(data, maskField, originalValue); + } + } + } + + @Override + public void verifyMaskFieldData(M data) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field field : maskFieldList) { + Object value = ReflectUtil.getFieldValue(data, field); + if (value != null) { + String maskChar = String.valueOf(field.getAnnotation(MaskField.class).maskChar()); + if (value.toString().contains(maskChar)) { + throw new MyRuntimeException("数据验证失败,字段 [" + field.getName() + "] 数据存在脱敏掩码字符!"); + } + } + } + } + + @Override + public CallResult verifyRelatedData(M data, M originalData) { + return CallResult.ok(); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRelatedData(M data) { + if (data == null) { + return CallResult.ok(); + } + Object id = ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + return this.verifyRelatedData(data, null); + } + M originalData = this.getById((K) id); + if (originalData == null) { + return CallResult.error("数据验证失败,源数据不存在!"); + } + return this.verifyRelatedData(data, originalData); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRelatedData(List dataList) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 1. 先过滤出数据列表中的主键Id集合。 + Set idList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> (K) ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + // 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。 + if (CollUtil.isNotEmpty(idList)) { + // 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。 + List originalList = this.getInList(idList); + Map originalMap = originalList.stream() + .collect(toMap(c -> ReflectUtil.getFieldValue(c, idFieldName), c2 -> c2)); + // 迭代列表,传入当前最新数据和更新前数据进行比对,如果关联数据变化了,就对新数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRelatedData( + data, originalMap.get(ReflectUtil.getFieldValue(data, idFieldName))); + if (!result.isSuccess()) { + return result; + } + } + } else { + // 4. 迭代列表,传入当前最新数据,对关联数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRelatedData(data, null); + if (!result.isSuccess()) { + return result; + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForConstDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + String errorMessage = StrFormatter.format("FieldName [{}] doesn't exist", fieldName); + throw new MyRuntimeException(errorMessage); + } + RelationConstDict relationConstDict = field.getAnnotation(RelationConstDict.class); + if (relationConstDict == null) { + String errorMessage = StrFormatter.format("FieldName [{}] doesn't have RelationConstDict.", fieldName); + throw new MyRuntimeException(errorMessage); + } + Method m = ReflectUtil.getMethodByName(relationConstDict.constantDictClass(), "isValid"); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null) { + boolean ok = ReflectUtil.invokeStatic(m, id); + if (!ok) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的常量字典值 [%s]!", + relationConstDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForGlobalDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] does not exist.", fieldName)); + } + RelationGlobalDict relationGlobalDict = field.getAnnotation(RelationGlobalDict.class); + if (relationGlobalDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationGlobalDict.", fieldName)); + } + RelationStruct relationStruct = this.relationGlobalDictStructList.stream() + .filter(c -> c.relationField.getName().equals(fieldName)).findFirst().orElse(null); + Assert.notNull(relationStruct, "GlobalDictRelationStruct for [" + fieldName + "] can't be NULL"); + Map dictMap = ReflectUtil.invoke( + relationStruct.service, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), null); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictMap.containsKey(id.toString())) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的全局编码字典值 [%s]!", + relationGlobalDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] does not exist.", fieldName)); + } + RelationDict relationDict = field.getAnnotation(RelationDict.class); + if (relationDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationDict.", fieldName)); + } + BaseService service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + Set dictIdSet = service.getAllList().stream() + .map(c -> ReflectUtil.getFieldValue(c, relationDict.slaveIdField())).collect(toSet()); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictIdSet.contains(id)) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的字典表字典值 [%s]!", + relationDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForDatasourceDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist.", fieldName)); + } + RelationDict relationDict = field.getAnnotation(RelationDict.class); + if (relationDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationDict.", fieldName)); + } + // 验证数据源字典Id,由于被依赖的数据表,可能包含大量业务数据,因此还是分批做存在性比对更为高效。 + Set idSet = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(toSet()); + if (CollUtil.isNotEmpty(idSet)) { + if (idSet.iterator().next() instanceof String) { + idSet = idSet.stream().filter(c -> StrUtil.isNotBlank((String) c)).collect(toSet()); + } + BaseService slaveService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + List notExistIdList = slaveService.notExist(relationDict.slaveIdField(), idSet, true); + if (CollUtil.isNotEmpty(notExistIdList)) { + R notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的数据源表字典值 [%s]!", + relationDict.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForOneToOneRelation(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist", fieldName)); + } + RelationOneToOne relationOneToOne = field.getAnnotation(RelationOneToOne.class); + if (relationOneToOne == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationOneToOne.", fieldName)); + } + // 验证一对一关联Id,由于被依赖的数据表,可能包含大量业务数据,因此还是分批做存在性比对更为高效。 + Set idSet = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(toSet()); + if (CollUtil.isNotEmpty(idSet)) { + BaseService slaveService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToOne.slaveServiceName(), relationOneToOne.slaveModelClass())); + List notExistIdList = slaveService.notExist(relationOneToOne.slaveIdField(), idSet, true); + if (CollUtil.isNotEmpty(notExistIdList)) { + R notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的一对一关联值 [%s]!", + relationOneToOne.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + */ + @Override + public void buildRelationForDataList(List resultList, MyRelationParam relationParam) { + this.buildRelationForDataList(resultList, relationParam, null); + } + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + @Override + public void buildRelationForDataList( + List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (relationParam == null || CollUtil.isEmpty(resultList)) { + return; + } + boolean dataFilterValue = GlobalThreadLocal.setDataFilter(false); + try { + // 集成本地一对一和字段级别的数据关联。 + boolean buildOneToOne = relationParam.isBuildOneToOne() || relationParam.isBuildOneToOneWithDict(); + // 这里集成一对一关联。 + if (buildOneToOne) { + this.buildOneToOneForDataList(resultList, relationParam, ignoreFields); + } + // 集成一对多关联 + if (relationParam.isBuildOneToMany()) { + this.buildOneToManyForDataList(resultList, relationParam, ignoreFields); + } + // 这里集成多对多关联。 + if (relationParam.isBuildRelationManyToMany()) { + this.buildManyToManyForDataList(resultList, ignoreFields); + } + // 这里集成字典关联 + if (relationParam.isBuildDict()) { + // 构建全局字典关联关系 + this.buildGlobalDictForDataList(resultList, ignoreFields); + // 构建常量字典关联关系 + this.buildConstDictForDataList(resultList, ignoreFields); + this.buildDictForDataList(resultList, buildOneToOne, ignoreFields); + } + // 组装本地聚合计算关联数据 + if (relationParam.isBuildRelationAggregation()) { + // 处理多对多场景下,根据主表的结果,进行从表聚合数据的计算。 + this.buildManyToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 处理多一多场景下,根据主表的结果,进行从表聚合数据的计算。 + this.buildOneToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + } finally { + GlobalThreadLocal.setDataFilter(dataFilterValue); + } + } + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + */ + @Override + public void buildRelationForDataList(List resultList, MyRelationParam relationParam, int batchSize) { + this.buildRelationForDataList(resultList, relationParam, batchSize, null); + } + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + @Override + public void buildRelationForDataList( + List resultList, MyRelationParam relationParam, int batchSize, Set ignoreFields) { + if (CollUtil.isEmpty(resultList)) { + return; + } + if (batchSize <= 0) { + this.buildRelationForDataList(resultList, relationParam); + return; + } + int totalCount = resultList.size(); + int fromIndex = 0; + int toIndex = Math.min(batchSize, totalCount); + while (toIndex > fromIndex) { + List subResultList = resultList.subList(fromIndex, toIndex); + this.buildRelationForDataList(subResultList, relationParam, ignoreFields); + fromIndex = toIndex; + toIndex = Math.min(batchSize + fromIndex, totalCount); + } + } + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param 实体对象类型。 + */ + @Override + public void buildRelationForData(T dataObject, MyRelationParam relationParam) { + this.buildRelationForData(dataObject, relationParam, null); + } + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @param 实体对象类型。 + */ + @Override + public void buildRelationForData(T dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || relationParam == null) { + return; + } + boolean dataFilterValue = GlobalThreadLocal.setDataFilter(false); + try { + // 集成本地一对一和字段级别的数据关联。 + boolean buildOneToOne = relationParam.isBuildOneToOne() || relationParam.isBuildOneToOneWithDict(); + if (buildOneToOne) { + this.buildOneToOneForData(dataObject, relationParam, ignoreFields); + } + // 集成一对多关联 + if (relationParam.isBuildOneToMany()) { + this.buildOneToManyForData(dataObject, relationParam, ignoreFields); + } + if (relationParam.isBuildDict()) { + // 构建全局字典关联关系 + this.buildGlobalDictForData(dataObject, ignoreFields); + // 构建常量字典关联关系 + this.buildConstDictForData(dataObject, ignoreFields); + // 构建本地数据字典关联关系。 + this.buildDictForData(dataObject, buildOneToOne, ignoreFields); + } + // 组装本地聚合计算关联数据 + if (relationParam.isBuildRelationAggregation()) { + // 开始处理多对多场景。 + buildManyToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 构建一对多场景 + buildOneToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + if (relationParam.isBuildRelationManyToMany()) { + this.buildRelationManyToMany(dataObject, ignoreFields); + } + } finally { + GlobalThreadLocal.setDataFilter(dataFilterValue); + } + } + + protected void buildLocalOneToOneDictOnly(T dataObject) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToOneStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationOneToOneStructList) { + BaseService relationService = relationStruct.service; + Object relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.relationField); + if (relationObject != null) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典 + proxyTarget.buildDictForData(relationObject, false, null); + // 关联全局字典 + proxyTarget.buildGlobalDictForData(relationObject, null); + // 关联常量字典 + proxyTarget.buildConstDictForData(relationObject, null); + } + } + } + + /** + * 集成主表和多对多中间表之间的关联关系。 + * + * @param dataObject 关联后的主表数据对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRelationManyToMany(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationManyToManyStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationManyToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + RelationManyToMany r = relationStruct.relationManyToMany; + String masterIdColumn = MyModelUtil.safeMapToColumnName(r.relationMasterIdField(), r.relationModelClass()); + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, idFieldName); + Map filterMap = new HashMap<>(1); + filterMap.put(masterIdColumn, masterIdValue); + List manyToManyList = relationStruct.manyToManyMapper.selectByMap(filterMap); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, manyToManyList); + } + } + + /** + * 为实体对象参数列表数据集成本地静态字典关联数据。 + * + * @param resultList 主表数据列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildConstDictForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.relationConstDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.relationConstDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + for (M dataObject : resultList) { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + String name = MapUtil.get(relationStruct.dictMap, id, String.class); + if (name != null) { + Map dictMap = new HashMap<>(2); + dictMap.put("id", id); + dictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, dictMap); + } + } + } + } + } + + /** + * 为实体对象参数列表数据集成全局字典关联数据。 + * + * @param resultList 主表数据列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildGlobalDictForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.relationGlobalDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.relationGlobalDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + Map dictMap = ReflectUtil.invoke( + relationStruct.service, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), masterIdSet); + MyModelUtil.makeGlobalDictRelation( + modelClass, resultList, dictMap, relationStruct.relationField.getName()); + } + } + } + + /** + * 为参数实体对象数据集成本地静态字典关联数据。 + * + * @param dataObject 实体对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildConstDictForData(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.relationConstDictStructList)) { + return; + } + for (RelationStruct relationStruct : this.relationConstDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + String name = MapUtil.get(relationStruct.dictMap, id, String.class); + if (name != null) { + Map dictMap = new HashMap<>(2); + dictMap.put("id", id); + dictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, dictMap); + } + } + } + } + + /** + * 为参数实体对象数据集成全局字典关联数据。 + * + * @param dataObject 实体对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildGlobalDictForData(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.relationGlobalDictStructList)) { + return; + } + for (RelationStruct relationStruct : this.relationGlobalDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + Map dictMap = ReflectUtil.invoke( + relationStruct.service, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), CollUtil.newHashSet(id)); + String name = dictMap.get(id.toString()); + if (name != null) { + Map reulstDictMap = new HashMap<>(2); + reulstDictMap.put("id", id); + reulstDictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, reulstDictMap); + } + } + } + } + + /** + * 为实体对象参数列表数据集成本地字典关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildDictForDataList(List resultList, boolean hasBuiltOneToOne, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + List relationList = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationList = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.equalOneToOneRelationField)) + .filter(Objects::nonNull) + .collect(toList()); + } else { + String slaveId = relationStruct.relationDict.slaveIdField(); + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + relationList = relationStruct.service.getInList(slaveId, masterIdSet); + } + } + MyModelUtil.makeDictRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + } + } + + /** + * 为实体对象数据集成本地数据字典关联数据。 + * + * @param dataObject 实体对象。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildDictForData(T dataObject, boolean hasBuiltOneToOne, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationDictStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object relationObject = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.equalOneToOneRelationField); + } else { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + relationObject = relationStruct.service.getOne(relationStruct.relationDict.slaveIdField(), id); + } + } + MyModelUtil.makeDictRelation( + modelClass, dataObject, relationObject, relationStruct.relationField.getName()); + } + } + + /** + * 为实体对象参数列表数据集成本地一对一关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param relationParam 关联从参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToOneForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToOneStructList) || CollUtil.isEmpty(resultList)) { + return; + } + boolean withDict = relationParam.isBuildOneToOneWithDict(); + for (RelationStruct relationStruct : this.localRelationOneToOneStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + BaseService relationService = relationStruct.service; + List relationList = + relationService.getInList(relationStruct.relationOneToOne.slaveIdField(), masterIdSet); + Set igoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + igoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + } + relationService.maskFieldDataList(relationList, igoreMaskFieldSet); + MyModelUtil.makeOneToOneRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + // 仅仅当需要加载从表字典关联时,才去加载。 + if (withDict && relationStruct.relationOneToOne.loadSlaveDict() && CollUtil.isNotEmpty(relationList)) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典。 + proxyTarget.buildDictForDataList(relationList, false, ignoreFields); + // 关联全局字典 + proxyTarget.buildGlobalDictForDataList(relationList, ignoreFields); + // 关联常量字典 + proxyTarget.buildConstDictForDataList(relationList, ignoreFields); + } + } + } + } + + /** + * 为实体对象数据集成本地一对一关联数据。 + * + * @param dataObject 实体对象。 + * @param relationParam 从表数据关联参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToOneForData(M dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToOneStructList)) { + return; + } + boolean withDict = relationParam.isBuildOneToOneWithDict(); + for (RelationStruct relationStruct : this.localRelationOneToOneStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + BaseService relationService = relationStruct.service; + Object relationObject = relationService.getOne(relationStruct.relationOneToOne.slaveIdField(), id); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + } + relationService.maskFieldData(relationObject, ignoreMaskFieldSet); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + // 仅仅当需要加载从表字典关联时,才去加载。 + if (withDict && relationStruct.relationOneToOne.loadSlaveDict() && relationObject != null) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典 + proxyTarget.buildDictForData(relationObject, false, ignoreFields); + // 关联全局字典 + proxyTarget.buildGlobalDictForData(relationObject, ignoreFields); + // 关联常量字典 + proxyTarget.buildConstDictForData(relationObject, ignoreFields); + } + } + } + } + + private void buildOneToManyForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToManyStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationOneToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + BaseService relationService = relationStruct.service; + List relationList = relationService.getInListWithRelation( + relationStruct.relationOneToMany.slaveIdField(), masterIdSet, MyRelationParam.dictOnly()); + MyModelUtil.makeOneToManyRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToMany.slaveModelClass().getSimpleName()); + } + for (M data : resultList) { + @SuppressWarnings("unchecked") + List relationDataList = + (List) ReflectUtil.getFieldValue(data, relationStruct.relationField.getName()); + relationService.maskFieldDataList(relationDataList, ignoreMaskFieldSet); + } + } + } + } + + private void buildOneToManyForData(M dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToManyStructList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationOneToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + BaseService relationService = relationStruct.service; + Set masterIdSet = new HashSet<>(1); + masterIdSet.add(id); + List relationObject = relationService.getInListWithRelation( + relationStruct.relationOneToMany.slaveIdField(), masterIdSet, MyRelationParam.dictOnly()); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToMany.slaveModelClass().getSimpleName()); + } + relationService.maskFieldDataList(relationObject, ignoreMaskFieldSet); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + } + } + } + + private void buildManyToManyForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationManyToManyStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RelationStruct relationStruct : this.localRelationManyToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, idFieldName)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + RelationManyToMany r = relationStruct.relationManyToMany; + String masterIdColumn = MyModelUtil.safeMapToColumnName(r.relationMasterIdField(), r.relationModelClass()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(masterIdColumn, masterIdSet); + List relationList = relationStruct.manyToManyMapper.selectList(queryWrapper); + MyModelUtil.makeManyToManyRelation( + modelClass, idFieldName, resultList, relationList, relationStruct.relationField.getName()); + } + } + } + + /** + * 根据实体对象参数列表和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationManyToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.localRelationManyToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationManyToManyAggrStructList) { + if (!CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + this.doBuildManyToManyAggregationForDataList(resultList, criteriaListMap, relationStruct); + } + } + } + + private void doBuildManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, RelationStruct relationStruct) { + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isEmpty(masterIdSet)) { + return; + } + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + // 提取关联中用到的各种字段和表数据。 + BasicAggregationRelationInfo basicRelationInfo = + this.parseBasicAggregationRelationInfo(relationStruct, criteriaListMap); + // 构建多表关联的where语句 + StringBuilder whereClause = new StringBuilder(256); + // 如果需要从表聚合计算或参与过滤,则需要把中间表和从表之间的关联条件加上。 + if (!basicRelationInfo.onlySelectRelationTable) { + whereClause.append(basicRelationInfo.relationTable) + .append(".") + .append(basicRelationInfo.relationSlaveColumn) + .append(" = ") + .append(basicRelationInfo.slaveTable) + .append(".") + .append(basicRelationInfo.slaveColumn); + } else { + whereClause.append("1 = 1"); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + inlistFilter.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + StringBuilder tableNames = new StringBuilder(64); + tableNames.append(basicRelationInfo.relationTable); + if (!basicRelationInfo.onlySelectRelationTable) { + tableNames.append(", ").append(basicRelationInfo.slaveTable); + } + List> aggregationMapList = + mapper().getGroupedListByCondition(tableNames.toString(), + basicRelationInfo.selectList, whereClause.toString(), basicRelationInfo.groupBy); + doMakeLocalAggregationData(aggregationMapList, resultList, relationStruct); + } + + /** + * 根据实体对象和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildManyToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationManyToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationManyToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationManyToManyAggrStructList) { + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue == null || CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + BasicAggregationRelationInfo basicRelationInfo = + this.parseBasicAggregationRelationInfo(relationStruct, criteriaListMap); + // 组装过滤条件 + String whereClause = this.makeManyToManyWhereClause( + relationStruct, masterIdValue, basicRelationInfo, criteriaListMap); + StringBuilder tableNames = new StringBuilder(64); + tableNames.append(basicRelationInfo.relationTable); + if (!basicRelationInfo.onlySelectRelationTable) { + tableNames.append(", ").append(basicRelationInfo.slaveTable); + } + List> aggregationMapList = + mapper().getGroupedListByCondition(tableNames.toString(), + basicRelationInfo.selectList, whereClause, basicRelationInfo.groupBy); + // 将查询后的结果回填到主表数据中。 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Object value = aggregationMapList.get(0).get(AGGREGATED_VALUE); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + + /** + * 根据实体对象参数列表和过滤条件,集成本地一对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + // 处理多一多场景下,根据主表的结果,进行从表聚合数据的计算。 + if (CollUtil.isEmpty(this.localRelationOneToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationOneToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + // 开始获取后面所需的各种关联数据。此部分今后可以移植到缓存中,无需每次计算。 + String slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + String slaveColumnName = MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTable, slaveColumnName, relation.slaveModelClass(), + slaveTable, relation.aggregationField(), relation.aggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + inlistFilter.setCriteria(relation.slaveModelClass(), + relation.slaveIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + List> aggregationMapList = + mapper().getGroupedListByCondition(slaveTable, selectList, criteriaString, groupBy); + doMakeLocalAggregationData(aggregationMapList, resultList, relationStruct); + } + } + } + + /** + * 根据实体对象和过滤条件,集成本地一对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationOneToManyAggrStructList.size()); + } + for (RelationStruct relationStruct : this.localRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + String slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + String slaveColumnName = + MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTable, slaveColumnName, relation.slaveModelClass(), + slaveTable, relation.aggregationField(), relation.aggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + String whereClause = this.makeOneToManyWhereClause( + relationStruct, masterIdValue, slaveColumnName, criteriaListMap); + // 获取分组聚合计算结果 + List> aggregationMapList = + mapper().getGroupedListByCondition(slaveTable, selectList, whereClause, groupBy); + // 将计算结果回填到主表关联字段 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Object value = aggregationMapList.get(0).get(AGGREGATED_VALUE); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + } + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 + */ + @Override + public void loadRelationStruct() { + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field f : fields) { + initializeRelationDictStruct(f); + initializeRelationStruct(f); + initializeRelationAggregationStruct(f); + } + } + + /** + * 缺省实现返回null,在进行一对多和多对多聚合计算时,没有额外的自定义过滤条件。如有需要,需子类自行实现。 + * + * @return 自定义过滤条件列表。 + */ + protected Map> buildAggregationAdditionalWhereCriteria() { + return null; + } + + /** + * 判断当前对象的关联字段数据是否需要被验证,如果原有对象为null,表示新对象第一次插入,则必须验证。 + * + * @param object 新对象。 + * @param originalObject 原有对象。 + * @param fieldGetter 获取需要验证字段的函数对象。 + * @param 需要验证字段的类型。 + * @return 需要关联验证返回true,否则false。 + */ + protected boolean needToVerify(M object, M originalObject, Function fieldGetter) { + if (object == null) { + return false; + } + T data = fieldGetter.apply(object); + if (data == null) { + return false; + } + if (data instanceof String stringData) { + if (stringData.isEmpty()) { + return false; + } + } + if (originalObject == null) { + return true; + } + T originalData = fieldGetter.apply(originalObject); + return !data.equals(originalData); + } + + /** + * 因为Mybatis Plus中QueryWrapper的条件方法都要求传入数据表字段名,因此提供该函数将 + * Java实体对象的字段名转换为数据表字段名,如果不存在会抛出异常。 + * 另外在MyModelUtil.mapToColumnName有一级缓存,对于查询过的对象字段都会放到缓存中, + * 下次映射转换的时候,会直接从缓存获取。 + * + * @param fieldName Java实体对象的字段名。 + * @return 对应的数据表字段名。 + */ + protected String safeMapToColumnName(String fieldName) { + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + if (columnName == null) { + throw new InvalidDataFieldException(modelClass.getSimpleName(), fieldName); + } + return columnName; + } + + /** + * 因为Mybatis Plus在update的时候,不能将实体对象中值为null的字段,更新为null, + * 而且忽略更新,在全部更新场景下,这个是非常重要的,所以我们写了这个函数绕开这一问题。 + * 该函数会遍历实体对象中,所有不包含@Transient注解,没有transient修饰符的字段,如果 + * 当前对象的该字段值为null,则会调用UpdateWrapper的set方法,将该字段赋值为null。 + * 相比于其他重载方法,该方法会将参数中的主键id,设置到UpdateWrapper的过滤条件中。 + * + * @param o 实体对象。 + * @param id 实体对象的主键值。 + * @return 创建后的UpdateWrapper。 + */ + protected UpdateWrapper createUpdateQueryForNullValue(M o, K id) { + UpdateWrapper uw = createUpdateQueryForNullValue(o, modelClass); + try { + M filter = modelClass.newInstance(); + this.setIdFieldMethod.invoke(filter, id); + uw.setEntity(filter); + } catch (Exception e) { + log.error("Failed to call reflection code of BaseService.createUpdateQueryForNullValue.", e); + throw new MyRuntimeException(e); + } + return uw; + } + + /** + * 因为Mybatis Plus在update的时候,不能将实体对象中值为null的字段,更新为null, + * 而且忽略更新,在全部更新场景下,这个是非常重要的,所以我们写了这个函数绕开这一问题。 + * 该函数会遍历实体对象中,所有不包含@Transient注解,没有transient修饰符的字段,如果 + * 当前对象的该字段值为null,则会调用UpdateWrapper的set方法,将该字段赋值为null。 + * + * @param o 实体对象。 + * @return 创建后的UpdateWrapper。 + */ + protected UpdateWrapper createUpdateQueryForNullValue(M o) { + return createUpdateQueryForNullValue(o, modelClass); + } + + /** + * 因为Mybatis Plus在update的时候,不能将实体对象中值为null的字段,更新为null, + * 而且忽略更新,在全部更新场景下,这个是非常重要的,所以我们写了这个函数绕开这一问题。 + * 该函数会遍历实体对象中,所有不包含@Transient注解,没有transient修饰符的字段,如果 + * 当前对象的该字段值为null,则会调用UpdateWrapper的set方法,将该字段赋值为null。 + * + * @param o 实体对象。 + * @param clazz 实体对象的class。 + * @return 创建后的UpdateWrapper。 + */ + public static UpdateWrapper createUpdateQueryForNullValue(T o, Class clazz) { + UpdateWrapper uw = new UpdateWrapper<>(); + Field[] fields = ReflectUtil.getFields(clazz); + List nullColumnList = new LinkedList<>(); + for (Field field : fields) { + TableField tableField = field.getAnnotation(TableField.class); + if (tableField == null || tableField.exist()) { + int modifiers = field.getModifiers(); + // transient类型的字段不能作为查询条件,静态字段和逻辑删除都不考虑。 + int transientMask = 128; + if ((modifiers & transientMask) == 1 + || Modifier.isStatic(modifiers) + || field.getAnnotation(TableLogic.class) != null) { + continue; + } + // 仅当实体对象参数中,当前字段值为null的时候,才会赋值给UpdateWrapper。 + // 以便在后续的更新中,可以将这些null字段的值设置到数据库表对应的字段中。 + if (ReflectUtil.getFieldValue(o, field) == null) { + nullColumnList.add(MyModelUtil.safeMapToColumnName(field.getName(), clazz)); + } + } + } + if (CollUtil.isNotEmpty(nullColumnList)) { + for (String nullColumn : nullColumnList) { + uw.set(nullColumn, null); + } + } + return uw; + } + + @SuppressWarnings("unchecked") + private void initializeRelationStruct(Field f) { + RelationOneToOne relationOneToOne = f.getAnnotation(RelationOneToOne.class); + if (relationOneToOne != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToOne.masterIdField()); + relationStruct.relationOneToOne = relationOneToOne; + if (!relationOneToOne.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationOneToOne.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToOne.slaveServiceName(), relationOneToOne.slaveModelClass())); + } + localRelationOneToOneStructList.add(relationStruct); + return; + } + RelationOneToMany relationOneToMany = f.getAnnotation(RelationOneToMany.class); + if (relationOneToMany != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToMany.masterIdField()); + relationStruct.relationOneToMany = relationOneToMany; + if (!relationOneToMany.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationOneToMany.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToMany.slaveServiceName(), relationOneToMany.slaveModelClass())); + } + localRelationOneToManyStructList.add(relationStruct); + return; + } + RelationManyToMany relationManyToMany = f.getAnnotation(RelationManyToMany.class); + if (relationManyToMany != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationManyToMany.relationMasterIdField()); + relationStruct.relationManyToMany = relationManyToMany; + String relationMapperName = relationManyToMany.relationMapperName(); + if (StrUtil.isBlank(relationMapperName)) { + relationMapperName = relationManyToMany.relationModelClass().getSimpleName() + "Mapper"; + } + relationStruct.manyToManyMapper = ApplicationContextHolder.getBean(StrUtil.lowerFirst(relationMapperName)); + localRelationManyToManyStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeRelationAggregationStruct(Field f) { + RelationOneToManyAggregation relationOneToManyAggregation = f.getAnnotation(RelationOneToManyAggregation.class); + if (relationOneToManyAggregation != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToManyAggregation.masterIdField()); + relationStruct.relationOneToManyAggregation = relationOneToManyAggregation; + if (!relationOneToManyAggregation.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationOneToManyAggregation.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationOneToManyAggregation.slaveServiceName(), relationOneToManyAggregation.slaveModelClass())); + } + localRelationOneToManyAggrStructList.add(relationStruct); + return; + } + RelationManyToManyAggregation relationManyToManyAggregation = f.getAnnotation(RelationManyToManyAggregation.class); + if (relationManyToManyAggregation != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationManyToManyAggregation.masterIdField()); + relationStruct.relationManyToManyAggregation = relationManyToManyAggregation; + if (!relationManyToManyAggregation.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationManyToManyAggregation.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationManyToManyAggregation.slaveServiceName(), relationManyToManyAggregation.slaveModelClass())); + } + localRelationManyToManyAggrStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeRelationDictStruct(Field f) { + RelationConstDict relationConstDict = f.getAnnotation(RelationConstDict.class); + if (relationConstDict != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationConstDict = relationConstDict; + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationConstDict.masterIdField()); + Field dictMapField = ReflectUtil.getField(relationConstDict.constantDictClass(), "DICT_MAP"); + relationStruct.dictMap = (Map) ReflectUtil.getStaticFieldValue(dictMapField); + relationConstDictStructList.add(relationStruct); + return; + } + RelationGlobalDict relationGlobalDict = f.getAnnotation(RelationGlobalDict.class); + if (relationGlobalDict != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationGlobalDict = relationGlobalDict; + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationGlobalDict.masterIdField()); + relationStruct.service = ApplicationContextHolder.getBean("globalDictService"); + relationStruct.globalDictMethd = ReflectUtil.getMethodByName( + relationStruct.service.getClass(), "getGlobalDictItemDictMapFromCache"); + relationGlobalDictStructList.add(relationStruct); + return; + } + RelationDict relationDict = f.getAnnotation(RelationDict.class); + if (relationDict != null) { + RelationStruct relationStruct = new RelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationDict.masterIdField()); + relationStruct.relationDict = relationDict; + if (StrUtil.isNotBlank(relationDict.equalOneToOneRelationField())) { + relationStruct.equalOneToOneRelationField = + ReflectUtil.getField(modelClass, relationDict.equalOneToOneRelationField()); + } + if (!relationDict.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.service = (BaseService) + ApplicationContextHolder.getBean(relationDict.slaveServiceClass()); + } else { + relationStruct.service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + } + localRelationDictStructList.add(relationStruct); + } + } + + private BasicAggregationRelationInfo parseBasicAggregationRelationInfo( + RelationStruct relationStruct, Map> criteriaListMap) { + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + BasicAggregationRelationInfo relationInfo = new BasicAggregationRelationInfo(); + // 提取关联中用到的各种字段和表数据。 + relationInfo.slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + relationInfo.relationTable = MyModelUtil.mapToTableName(relation.relationModelClass()); + relationInfo.relationMasterColumn = + MyModelUtil.mapToColumnName(relation.relationMasterIdField(), relation.relationModelClass()); + relationInfo.relationSlaveColumn = + MyModelUtil.mapToColumnName(relation.relationSlaveIdField(), relation.relationModelClass()); + relationInfo.slaveColumn = MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + // 判断是否只需要关联中间表即可,从而提升查询统计的效率。 + // 1. 统计字段为中间表字段。2. 自定义过滤条件中没有基于从表字段的过滤条件。 + relationInfo.onlySelectRelationTable = + relation.aggregationModelClass().equals(relation.relationModelClass()); + if (relationInfo.onlySelectRelationTable && MapUtil.isNotEmpty(criteriaListMap)) { + List criteriaList = + criteriaListMap.get(relationStruct.relationField.getName()); + if (CollUtil.isNotEmpty(criteriaList)) { + for (MyWhereCriteria whereCriteria : criteriaList) { + if (whereCriteria.getModelClazz().equals(relation.slaveModelClass())) { + relationInfo.onlySelectRelationTable = false; + break; + } + } + } + } + String aggregationTable = relation.aggregationModelClass().equals(relation.relationModelClass()) + ? relationInfo.relationTable : relationInfo.slaveTable; + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + relationInfo.relationTable, relationInfo.relationMasterColumn, relation.aggregationModelClass(), + aggregationTable, relation.aggregationField(), relation.aggregationType()); + relationInfo.selectList = selectAndGroupByTuple.getFirst(); + relationInfo.groupBy = selectAndGroupByTuple.getSecond(); + return relationInfo; + } + + private String makeManyToManyWhereClause( + RelationStruct relationStruct, + Object masterIdValue, + BasicAggregationRelationInfo basicRelationInfo, + Map> criteriaListMap) { + StringBuilder whereClause = new StringBuilder(256); + whereClause.append(basicRelationInfo.relationTable) + .append(".").append(basicRelationInfo.relationMasterColumn); + if (masterIdValue instanceof Number) { + whereClause.append(" = ").append(masterIdValue); + } else { + whereClause.append(" = '").append(masterIdValue).append("'"); + } + // 如果需要从表聚合计算或参与过滤,则需要把中间表和从表之间的关联条件加上。 + if (!basicRelationInfo.onlySelectRelationTable) { + whereClause.append(AND_OP) + .append(basicRelationInfo.relationTable) + .append(".") + .append(basicRelationInfo.relationSlaveColumn) + .append(" = ") + .append(basicRelationInfo.slaveTable) + .append(".") + .append(basicRelationInfo.slaveColumn); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relationStruct.relationManyToManyAggregation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (CollUtil.isNotEmpty(criteriaList)) { + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + } + return whereClause.toString(); + } + + private String makeOneToManyWhereClause( + RelationStruct relationStruct, + Object masterIdValue, + String slaveColumnName, + Map> criteriaListMap) { + StringBuilder whereClause = new StringBuilder(64); + if (masterIdValue instanceof Number) { + whereClause.append(slaveColumnName).append(" = ").append(masterIdValue); + } else { + whereClause.append(slaveColumnName).append(" = '").append(masterIdValue).append("'"); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + if (StrUtil.isNotBlank(relationStruct.service.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relationStruct.relationOneToManyAggregation.slaveModelClass(), + relationStruct.service.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (CollUtil.isNotEmpty(criteriaList)) { + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + } + return whereClause.toString(); + } + + private static class BasicAggregationRelationInfo { + private String slaveTable; + private String slaveColumn; + private String relationTable; + private String relationMasterColumn; + private String relationSlaveColumn; + private String selectList; + private String groupBy; + private boolean onlySelectRelationTable; + } + + private void doMakeLocalAggregationData( + List> aggregationMapList, List resultList, RelationStruct relationStruct) { + if (CollUtil.isEmpty(resultList)) { + return; + } + // 根据获取的分组聚合结果集,绑定到主表总的关联字段。 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Map relatedMap = new HashMap<>(aggregationMapList.size()); + String groupedKey = GROUPED_KEY; + String aggregatedValue = AGGREGATED_VALUE; + if (!aggregationMapList.get(0).containsKey(groupedKey)) { + groupedKey = groupedKey.toLowerCase(); + aggregatedValue = aggregatedValue.toLowerCase(); + } + for (Map map : aggregationMapList) { + relatedMap.put(map.get(groupedKey).toString(), map.get(aggregatedValue)); + } + for (M dataObject : resultList) { + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + Object value = relatedMap.get(masterIdValue.toString()); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + } + + private Tuple2 makeSelectListAndGroupByClause( + String groupTableName, + String groupColumnName, + Class aggregationModel, + String aggregationTableName, + String aggregationField, + Integer aggregationType) { + if (!AggregationType.isValid(aggregationType)) { + throw new IllegalArgumentException("Invalid AggregationType Value [" + + aggregationType + "] in Model [" + aggregationModel.getName() + "]."); + } + String aggregationFunc = AggregationType.getAggregationFunction(aggregationType); + String aggregationColumn = MyModelUtil.mapToColumnName(aggregationField, aggregationModel); + if (StrUtil.isBlank(aggregationColumn)) { + throw new IllegalArgumentException("Invalid AggregationField [" + + aggregationField + "] in Model [" + aggregationModel.getName() + "]."); + } + // 构建Select List + // 如:r_table.master_id groupedKey, SUM(r_table.aggr_column) aggregated_value + StringBuilder groupedSelectList = new StringBuilder(128); + groupedSelectList.append(groupTableName) + .append(".") + .append(groupColumnName) + .append(" ") + .append(GROUPED_KEY) + .append(", ") + .append(aggregationFunc) + .append("(") + .append(aggregationTableName) + .append(".") + .append(aggregationColumn) + .append(") ") + .append(AGGREGATED_VALUE) + .append(" "); + StringBuilder groupBy = new StringBuilder(64); + groupBy.append(groupTableName).append(".").append(groupColumnName); + return new Tuple2<>(groupedSelectList.toString(), groupBy.toString()); + } + + private Object doMaskFieldData(M data, Field maskField, MaskField anno) { + Object value = ReflectUtil.getFieldValue(data, maskField); + if (value == null) { + return value; + } + if (anno.maskType().equals(MaskFieldTypeEnum.NAME)) { + value = MaskFieldUtil.chineseName(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.MOBILE_PHONE)) { + value = MaskFieldUtil.mobilePhone(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.FIXED_PHONE)) { + value = MaskFieldUtil.fixedPhone(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.EMAIL)) { + value = MaskFieldUtil.email(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.ID_CARD)) { + value = MaskFieldUtil.idCardNum(value.toString(), anno.noMaskPrefix(), anno.noMaskSuffix(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.BANK_CARD)) { + value = MaskFieldUtil.bankCard(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.CAR_LICENSE)) { + value = MaskFieldUtil.carLicense(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.CUSTOM)) { + MaskFieldHandler handler = + maskFieldHandlerMap.computeIfAbsent(anno.handler(), ApplicationContextHolder::getBean); + value = handler.handleMask(modelClass.getSimpleName(), maskField.getName(), value.toString(), anno.maskChar()); + } + return value; + } + + private void compareAndSetMaskFieldData(M data, K id) { + if (CollUtil.isNotEmpty(maskFieldList)) { + M originalData = this.getById(id); + this.compareAndSetMaskFieldData(data, originalData); + } + } + + private String getNormalizedSlaveServiceName(String slaveServiceName, Class slaveModelClass) { + if (StrUtil.isBlank(slaveServiceName)) { + slaveServiceName = slaveModelClass.getSimpleName() + "Service"; + } + return StrUtil.lowerFirst(slaveServiceName); + } + + @Data + public static class RelationStruct { + private Field relationField; + private Field masterIdField; + private Field equalOneToOneRelationField; + private Method globalDictMethd; + private BaseService service; + private BaseDaoMapper manyToManyMapper; + private Map dictMap; + private RelationConstDict relationConstDict; + private RelationGlobalDict relationGlobalDict; + private RelationDict relationDict; + private RelationOneToOne relationOneToOne; + private RelationOneToMany relationOneToMany; + private RelationManyToMany relationManyToMany; + private RelationOneToManyAggregation relationOneToManyAggregation; + private RelationManyToManyAggregation relationManyToManyAggregation; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java new file mode 100644 index 00000000..556b70b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseDictService.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.core.base.service; + +import java.io.Serializable; +import java.util.List; + +/** + * 带有缓存功能的字典Service接口。 + * + * @param Model实体对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface IBaseDictService extends IBaseService { + + /** + * 重新加载数据库中所有当前表数据到系统内存。 + * + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + void reloadCachedData(boolean force); + + /** + * 保存新增对象。 + * + * @param data 新增对象。 + * @return 返回新增对象。 + */ + M saveNew(M data); + + /** + * 更新数据对象。 + * + * @param data 更新的对象。 + * @param originalData 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(M data, M originalData); + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(K id); + + /** + * 直接从缓存池中获取所有数据。 + * + * @return 返回所有数据。 + */ + List getAllListFromCache(); + + /** + * 根据父主键Id,获取子对象列表。 + * + * @param parentId 上级行政区划Id。 + * @return 下级行政区划列表。 + */ + List getListByParentId(K parentId); + + /** + * 获取缓存中的数据数量。 + * + * @return 缓存中的数据总量。 + */ + int getCachedCount(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java new file mode 100644 index 00000000..953980bc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/service/IBaseService.java @@ -0,0 +1,559 @@ +package com.orangeforms.common.core.base.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TableModelInfo; + +import java.io.Serializable; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * 所有Service的接口。 + * + * @param Model对象的类型。 + * @param Model对象主键的类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface IBaseService extends IService { + + /** + * 如果主键存在则更新,否则新增保存实体对象。 + * + * @param data 实体对象数据。 + * @param saveNew 新增实体对象方法。 + * @param update 更新实体对象方法。 + */ + void saveNewOrUpdate(M data, Consumer saveNew, BiConsumer update); + + /** + * 如果主键存在的则更新,否则批量新增保存实体对象。 + * + * @param dataList 实体对象数据列表。 + * @param saveNewBatch 批量新增实体对象方法。 + * @param update 更新实体对象方法。 + */ + void saveNewOrUpdateBatch(List dataList, Consumer> saveNewBatch, BiConsumer update); + + /** + * 根据过滤条件删除数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + Integer removeBy(M filter); + + /** + * 基于主从表之间的关联字段,批量改更新一对多从表数据。 + * 该操作会覆盖增、删、改三个操作,具体如下: + * 1. 先删除。从表中relationFieldName字段的值为relationFieldValue, 同时主键Id不在dataList中的。 + * 2. 再批量插入。遍历dataList中没有主键Id的对象,视为新对象批量插入。 + * 3. 最后逐条更新,遍历dataList中有主键Id的对象,视为已存在对象并逐条更新。 + * 4. 如果更新时间和更新用户Id为空,我们将视当前记录为变化数据,因此使用当前时间和用户分别填充这两个字段。 + * + * @param relationFieldName 主从表关联中,从表的Java字段名。 + * @param relationFieldValue 主从表关联中,与从表关联的主表字段值。该值会被赋值给从表关联字段。 + * @param updateUserIdFieldName 一对多从表的更新用户Id字段名。 + * @param updateTimeFieldName 一对多从表的更新时间字段名 + * @param dataList 批量更新的从表数据列表。 + * @param batchInserter 从表批量插入方法。 + */ + void updateBatchOneToManyRelation( + String relationFieldName, + Object relationFieldValue, + String updateUserIdFieldName, + String updateTimeFieldName, + List dataList, + Consumer> batchInserter); + + /** + * 判断指定字段的数据是否存在,且仅仅存在一条记录。 + * 如果是基于主键的过滤,会直接调用existId过滤函数,提升性能。在有缓存的场景下,也可以利用缓存。 + * + * @param fieldName 待过滤的字段名(Java 字段)。 + * @param fieldValue 字段值。 + * @return 存在且仅存在一条返回true,否则false。 + */ + boolean existOne(String fieldName, Object fieldValue); + + /** + * 判断主键Id关联的数据是否存在。 + * + * @param id 主键Id。 + * @return 存在返回true,否则false。 + */ + boolean existId(K id); + + /** + * 返回符合过滤条件的一条数据。 + * + * @param filter 过滤的Java对象。 + * @return 查询后的数据对象。 + */ + M getOne(M filter); + + /** + * 返回符合 filterField = filterValue 条件的一条数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterValue 过滤的Java字段值。 + * @return 查询后的数据对象。 + */ + M getOne(String filterField, Object filterValue); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param id 主表主键Id。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 查询结果对象。 + */ + M getByIdWithRelation(K id, MyRelationParam relationParam); + + /** + * 获取所有数据。 + * + * @return 返回所有数据。 + */ + List getAllList(); + + /** + * 获取排序后所有数据。 + * + * @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 + * @return 返回排序后所有数据。 + */ + List getAllListByOrder(String... orderByProperties); + + /** + * 判断参数值主键集合中的所有数据,是否全部存在 + * + * @param idSet 待校验的主键集合。 + * @return 全部存在返回true,否则false。 + */ + boolean existAllPrimaryKeys(Set idSet); + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id + * @param inFilterValues 数据值列表。 + * @return 全部存在返回true,否则false。 + */ + boolean existUniqueKeyList(String inFilterField, Set inFilterValues); + + /** + * 根据过滤字段和过滤集合,返回不存在的数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterSet 过滤字段数据集合。 + * @param findFirst 是否找到第一个就返回。 + * @param 过滤字段类型。 + * @return filterSet中,在从表中不存在的数据集合。 + */ + List notExist(String filterField, Set filterSet, boolean findFirst); + + /** + * 返回符合主键 IN (idValues) 条件的所有数据。 + * + * @param idValues 主键值集合。 + * @return 检索后的数据列表。 + */ + List getInList(Set idValues); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + List getInList(String inFilterField, Set inFilterValues); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @return 检索后的数据列表。 + */ + List getInList(String inFilterField, Set inFilterValues, String orderBy); + + /** + * 返回符合主键 IN (idValues) 条件的所有数据。同时返回关联数据。 + * + * @param idValues 主键值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation(Set idValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。同时返回关联数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam); + + /** + * 返回符合主键 NOT IN (idValues) 条件的所有数据。 + * + * @param idValues 主键值集合。 + * @return 检索后的数据列表。 + */ + List getNotInList(Set idValues); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + List getNotInList(String inFilterField, Set inFilterValues); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @return 检索后的数据列表。 + */ + List getNotInList(String inFilterField, Set inFilterValues, String orderBy); + + /** + * 返回符合主键 NOT IN (idValues) 条件的所有数据。同时返回关联数据。 + * + * @param idValues 主键值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation(Set idValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。同时返回关联数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam); + + /** + * 用参数对象作为过滤条件,获取数据数量。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。 + * @return 返回过滤后的数据数量。 + */ + long getCountByFilter(M filter); + + /** + * 用参数对象作为过滤条件,判断是否存在过滤数据。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。 + * @return 存在返回true,否则false。 + */ + boolean existByFilter(M filter); + + /** + * 用参数对象作为过滤条件,获取查询结果。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。如果参数为null,则返回全部数据。 + * @return 返回过滤后的数据。 + */ + List getListByFilter(M filter); + + /** + * 用参数对象作为过滤条件,获取查询结果。同时查询并绑定关联数据。 + * + * @param filter 该方法基于mybatis的通用mapper。如果参数为null,则返回全部数据。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 返回过滤后的数据。 + */ + List getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam); + + /** + * 获取父主键Id下的所有子数据列表。 + * + * @param parentIdFieldName 父主键字段名字,如"courseId"。 + * @param parentId 父主键的值。 + * @return 父主键Id下的所有子数据列表。 + */ + List getListByParentId(String parentIdFieldName, K parentId); + + /** + * 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectFields 选择的字段列表,多个字段逗号分隔。 + * NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 + * 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy SQL常量形式分组字段列表,逗号分隔。 + * @return 聚合计算后的数据结果集。 + */ + List> getGroupedListByCondition(String selectFields, String whereClause, String groupBy); + + /** + * 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 + * @param filter 过滤对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param orderBy SQL常量形式排序字段列表,逗号分隔。 + * @return 查询结果。 + */ + List getListByCondition(List selectList, M filter, String whereClause, String orderBy); + + /** + * 用指定过滤条件,计算记录数量。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param whereClause SQL常量形式的条件从句。 + * @return 返回过滤后的数据数量。 + */ + Integer getCountByCondition(String whereClause); + + /** + * 仅对标记MaskField注解的字段数据进行脱敏。 + * + * @param data 实体对象。 + * @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 + */ + void maskFieldData(M data, Set ignoreFieldSet); + + /** + * 仅对标记MaskField注解的字段数据进行脱敏。 + * + * @param dataList 实体对象列表。 + * @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 + */ + void maskFieldDataList(List dataList, Set ignoreFieldSet); + + /** + * 比较并处理脱敏字段的数据变化。 + * 如果data对象中的脱敏字段值和originalData字段的脱敏后值相同,表示当前data对象的脱敏字段数据没有变化, + * 因此需要使用数据库中的原有字段值,覆盖当前实体对象中的该字段值,以保证数据库表字段中始终存储的是未脱敏数据。 + * + * @param data 当前数据对象。 + * @param originalData 原数据对象。 + */ + void compareAndSetMaskFieldData(M data, M originalData); + + /** + * 对标记MaskField注解的脱敏字段进行判断。字段数据中不能包含脱敏掩码字符。 + * + * @param data 实体对象。 + */ + void verifyMaskFieldData(M data); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中会给出返回CallResult.ok()的缺省实现。每个业务服务实现类在需要的时候可以重载该方法。 + * + * @param data 数据对象。 + * @param originalData 原有数据对象,null表示data为新增对象。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(M data, M originalData); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * 如果data对象中包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param data 数据对象。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(M data); + + /** + * 根据最新对象列表和原有对象列表的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * 如果dataList列表中的对象包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param dataList 数据对象列表。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(List dataList); + + /** + * 批量导入数据列表,对依赖全局字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖全局字典的字段名,包含RelationGlobalDict注解的字段。 + * @param idGetter 获取业务主表中依赖全局字典字段值的Function对象。 + * @param 业务主表中依全局字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForGlobalDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖常量字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖常量字典的字段名,包含RelationConstDict注解的字段。 + * @param idGetter 获取业务主表中依赖常量字典字段值的Function对象。 + * @param 业务主表中依赖常量字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForConstDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖字典表字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖字典表字典的字段名,包含RelationDict注解的字段。 + * @param idGetter 获取业务主表中依赖字典表字典字段值的Function对象。 + * @param 业务主表中依赖字典表字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖数据源字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖数据源字典的字段名,包含RelationDict注解的字段的数据源字典。 + * @param idGetter 获取业务主表中依赖数据源字典字段值的Function对象。 + * @param 业务主表中依赖数据源字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForDatasourceDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对存在一对一关联的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中存在一对一关联的字段名,包含RelationOneToOne注解的字段。 + * @param idGetter 获取业务主表中一对一关联字段值的Function对象。 + * @param 业务主表中存在一对一关联的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForOneToOneRelation(List dataList, String fieldName, Function idGetter); + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam); + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields); + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam, int batchSize); + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + void buildRelationForDataList( + List resultList, MyRelationParam relationParam, int batchSize, Set ignoreFields); + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param 实体对象类型。 + */ + void buildRelationForData(T dataObject, MyRelationParam relationParam); + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @param 实体对象类型。 + */ + void buildRelationForData(T dataObject, MyRelationParam relationParam, Set ignoreFields); + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 + */ + void loadRelationStruct(); + + /** + * 获取当前服务引用的实体对象及表信息。 + * + * @return 实体对象及表信息。 + */ + TableModelInfo getTableModelInfo(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java new file mode 100644 index 00000000..a4313a53 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/base/vo/BaseVo.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.core.base.vo; + +import lombok.Data; + +import java.util.Date; + +/** + * VO对象的公共基类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class BaseVo { + + /** + * 创建者Id。 + */ + private Long createUserId; + + /** + * 创建时间。 + */ + private Date createTime; + + /** + * 更新者Id。 + */ + private Long updateUserId; + + /** + * 更新时间。 + */ + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java new file mode 100644 index 00000000..203eafd1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/CacheConfig.java @@ -0,0 +1,110 @@ +package com.orangeforms.common.core.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * 使用Caffeine作为本地缓存库 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@EnableCaching +public class CacheConfig { + + private static final int DEFAULT_MAXSIZE = 10000; + private static final int DEFAULT_TTL = 3600; + + /** + * 定义cache名称、超时时长秒、最大个数 + * 每个cache缺省3600秒过期,最大个数1000 + */ + public enum CacheEnum { + /** + * 专门存储用户权限的缓存(600秒)。 + */ + USER_PERMISSION_CACHE(600, 10000), + /** + * 专门存储用户权限字的缓存(600秒)。仅当使用satoken权限框架时可用。 + */ + USER_PERM_CODE_CACHE(600, 10000), + /** + * 专门存储用户数据权限的缓存(600秒)。 + */ + DATA_PERMISSION_CACHE(600, 10000), + /** + * 专门存储用户菜单关联权限的缓存(600秒)。 + */ + MENU_PERM_CACHE(600, 10000), + /** + * 存储指定部门Id集合的所有子部门Id集合。 + */ + CHILDREN_DEPT_ID_CACHE(1800, 10000), + /** + * 在线表单组件渲染数据缓存。 + */ + ONLINE_FORM_RENDER_CACCHE(300, 100), + /** + * 报表表单组件渲染数据缓存。 + */ + REPORT_FORM_RENDER_CACCHE(300, 100), + /** + * 缺省全局缓存(时间是24小时)。 + */ + GLOBAL_CACHE(86400, 20000); + + CacheEnum() { + } + + CacheEnum(int ttl, int maxSize) { + this.ttl = ttl; + this.maxSize = maxSize; + } + + /** + * 缓存的最大数量。 + */ + private int maxSize = DEFAULT_MAXSIZE; + /** + * 缓存的时长(单位:秒) + */ + private int ttl = DEFAULT_TTL; + + public int getMaxSize() { + return maxSize; + } + + public int getTtl() { + return ttl; + } + } + + /** + * 初始化缓存配置。这里为了有别于Redisson的缓存。 + */ + @Bean("caffeineCacheManager") + public CacheManager cacheManager() { + SimpleCacheManager manager = new SimpleCacheManager(); + // 把各个cache注册到cacheManager中,CaffeineCache实现了org.springframework.cache.Cache接口 + ArrayList caches = new ArrayList<>(); + for (CacheEnum c : CacheEnum.values()) { + caches.add(new CaffeineCache(c.name(), + Caffeine.newBuilder().recordStats() + .expireAfterWrite(c.getTtl(), TimeUnit.SECONDS) + .maximumSize(c.getMaxSize()) + .build()) + ); + } + manager.setCaches(caches); + return manager; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java new file mode 100644 index 00000000..14fe0391 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/DictionaryCache.java @@ -0,0 +1,89 @@ +package com.orangeforms.common.core.cache; + +import java.util.List; +import java.util.Set; + +/** + * 主要用于完整缓存字典表数据的接口对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +public interface DictionaryCache { + + /** + * 按照数据插入的顺序返回全部字典对象的列表。 + * + * @return 全部字段数据列表。 + */ + List getAll(); + + /** + * 获取缓存中与键列表对应的对象列表。 + * + * @param keys 主键集合。 + * @return 对象列表。 + */ + List getInList(Set keys); + + /** + * 重新加载。如果数据列表为空,则会清空原有缓存数据。 + * + * @param dataList 待缓存的数据列表。 + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + void reload(List dataList, boolean force); + + /** + * 从缓存中获取指定的数据。 + * + * @param key 数据的key。 + * @return 获取到的数据,如果没有返回null。 + */ + V get(K key); + + /** + * 将数据存入缓存。 + * + * @param key 通常为字典数据的主键。 + * @param object 字典数据对象。 + */ + void put(K key, V object); + + /** + * 获取缓存中数据条目的数量。 + * + * @return 返回缓存的数据数量。 + */ + int getCount(); + + /** + * 删除缓存中指定的键。 + * + * @param key 待删除数据的主键。 + * @return 返回被删除的对象,如果主键不存在,返回null。 + */ + V invalidate(K key); + + /** + * 删除缓存中,参数列表中包含的键。 + * + * @param keys 待删除数据的主键集合。 + */ + void invalidateSet(Set keys); + + /** + * 清空缓存。 + */ + void invalidateAll(); + + /** + * 根据父主键Id获取所有子对象的列表。 + * + * @param parentId 父主键Id。如果parentId为null,则返回所有一级节点数据。 + * @return 所有子对象的列表。 + */ + default List getListByParentId(K parentId) { throw new UnsupportedOperationException(); } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java new file mode 100644 index 00000000..7f238801 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapDictionaryCache.java @@ -0,0 +1,200 @@ +package com.orangeforms.common.core.cache; + +import cn.hutool.core.map.MapUtil; +import com.orangeforms.common.core.exception.MapCacheAccessException; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 字典数据内存缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MapDictionaryCache implements DictionaryCache { + + /** + * 存储字典数据的Map。 + */ + protected final LinkedHashMap dataMap = new LinkedHashMap<>(); + /** + * 获取字典主键数据的函数对象。 + */ + protected final Function idGetter; + /** + * 由于大部分场景是读取操作,所以使用读写锁提高并发的伸缩性。 + */ + protected final ReadWriteLock lock = new ReentrantReadWriteLock(); + /** + * 超时时长。单位毫秒。 + */ + protected static final long TIMEOUT = 2000L; + + /** + * 当前对象的构造器函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的字典内存缓存对象。 + */ + public static MapDictionaryCache create(Function idGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + return new MapDictionaryCache<>(idGetter); + } + + /** + * 构造函数。 + * + * @param idGetter 主键Id的获取函数对象。 + */ + public MapDictionaryCache(Function idGetter) { + this.idGetter = idGetter; + } + + @Override + public List getAll() { + return this.safeRead("getAll", () -> { + List resultList = new LinkedList<>(); + if (MapUtil.isNotEmpty(dataMap)) { + resultList.addAll(dataMap.values()); + } + return resultList; + }); + } + + @Override + public List getInList(Set keys) { + return this.safeRead("getInList", () -> { + List resultList = new LinkedList<>(); + keys.forEach(key -> { + V object = dataMap.get(key); + if (object != null) { + resultList.add(object); + } + }); + return resultList; + }); + } + + @Override + public V get(K id) { + if (id == null) { + return null; + } + return this.safeRead("get", () -> dataMap.get(id)); + } + + @Override + public void reload(List dataList, boolean force) { + if (!force && this.getCount() > 0) { + return; + } + this.safeWrite("reload", () -> { + dataMap.clear(); + dataList.forEach(dataObj -> { + K id = idGetter.apply(dataObj); + dataMap.put(id, dataObj); + }); + return null; + }); + } + + @Override + public void put(K id, V object) { + this.safeWrite("put", () -> dataMap.put(id, object)); + } + + @Override + public int getCount() { + return dataMap.size(); + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + return this.safeWrite("invalidate", () -> dataMap.remove(id)); + } + + @Override + public void invalidateSet(Set keys) { + this.safeWrite("invalidateSet", () -> { + keys.forEach(id -> { + if (id != null) { + dataMap.remove(id); + } + }); + return null; + }); + } + + @Override + public void invalidateAll() { + this.safeWrite("invalidateAll", () -> { + dataMap.clear(); + return null; + }); + } + + protected T safeRead(String functionName, Supplier supplier) { + String exceptionMessage; + try { + if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { + try { + return supplier.get(); + } finally { + lock.readLock().unlock(); + } + } else { + throw new TimeoutException(); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + exceptionMessage = String.format( + "LOCK Operation of [MapDictionaryCache::%s] encountered EXCEPTION [%s] for DICT.", + functionName, e.getClass().getSimpleName()); + log.warn(exceptionMessage); + throw new MapCacheAccessException(exceptionMessage, e); + } + } + + protected T safeWrite(String functionName, Supplier supplier) { + String exceptionMessage; + try { + if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { + try { + return supplier.get(); + } finally { + lock.writeLock().unlock(); + } + } else { + throw new TimeoutException(); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + exceptionMessage = String.format( + "LOCK Operation of [MapDictionaryCache::%s] encountered EXCEPTION [%s] for DICT.", + functionName, e.getClass().getSimpleName()); + log.warn(exceptionMessage); + throw new MapCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java new file mode 100644 index 00000000..b492ebe2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/cache/MapTreeDictionaryCache.java @@ -0,0 +1,138 @@ +package com.orangeforms.common.core.cache; + +import cn.hutool.core.collection.CollUtil; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; + +/** + * 树形字典数据内存缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MapTreeDictionaryCache extends MapDictionaryCache { + + /** + * 树形数据存储对象。 + */ + private final Multimap allTreeMap = LinkedHashMultimap.create(); + /** + * 获取字典父主键数据的函数对象。 + */ + protected final Function parentIdGetter; + + /** + * 当前对象的构造器函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的树形字典内存缓存对象。 + */ + public static MapTreeDictionaryCache create(Function idGetter, Function parentIdGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + if (parentIdGetter == null) { + throw new IllegalArgumentException("ParentIdGetter can't be NULL."); + } + return new MapTreeDictionaryCache<>(idGetter, parentIdGetter); + } + + /** + * 构造函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + */ + public MapTreeDictionaryCache(Function idGetter, Function parentIdGetter) { + super(idGetter); + this.parentIdGetter = parentIdGetter; + } + + @Override + public void reload(List dataList, boolean force) { + if (!force && this.getCount() > 0) { + return; + } + this.safeWrite("reload", () -> { + dataMap.clear(); + allTreeMap.clear(); + dataList.forEach(data -> { + K id = idGetter.apply(data); + dataMap.put(id, data); + K parentId = parentIdGetter.apply(data); + allTreeMap.put(parentId, data); + }); + return null; + }); + } + + @Override + public List getListByParentId(K parentId) { + return this.safeRead("getListByParentId", () -> { + List resultList = new LinkedList<>(); + Collection children = allTreeMap.get(parentId); + if (CollUtil.isNotEmpty(children)) { + resultList.addAll(children); + } + return resultList; + }); + } + + @Override + public void put(K id, V data) { + this.safeWrite("put", () -> { + dataMap.put(id, data); + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, data); + allTreeMap.put(parentId, data); + return null; + }); + } + + @Override + public V invalidate(K id) { + return this.safeWrite("invalidate", () -> { + V v = dataMap.remove(id); + if (v != null) { + K parentId = parentIdGetter.apply(v); + allTreeMap.remove(parentId, v); + } + return v; + }); + } + + @Override + public void invalidateSet(Set keys) { + this.safeWrite("invalidateSet", () -> { + keys.forEach(id -> { + if (id != null) { + V data = dataMap.remove(id); + if (data != null) { + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, data); + } + } + }); + return null; + }); + } + + @Override + public void invalidateAll() { + this.safeWrite("invalidateAll", () -> { + dataMap.clear(); + allTreeMap.clear(); + return null; + }); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java new file mode 100644 index 00000000..369fcf33 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/BaseMultiDataSourceConfig.java @@ -0,0 +1,60 @@ +package com.orangeforms.common.core.config; + +import com.alibaba.druid.pool.DruidDataSource; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 基于Druid的数据源配置的基类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "spring.datasource.druid") +public class BaseMultiDataSourceConfig { + + private String driverClassName; + private String name; + private Integer initialSize; + private Integer minIdle; + private Integer maxActive; + private Integer maxWait; + private Integer timeBetweenEvictionRunsMillis; + private Integer minEvictableIdleTimeMillis; + private Boolean poolPreparedStatements; + private Integer maxPoolPreparedStatementPerConnectionSize; + private Integer maxOpenPreparedStatements; + private String validationQuery; + private Boolean testWhileIdle; + private Boolean testOnBorrow; + private Boolean testOnReturn; + + /** + * 将连接池的通用配置应用到数据源对象上。 + * + * @param druidDataSource Druid的数据源。 + * @return 应用后的Druid数据源。 + */ + protected DruidDataSource applyCommonProps(DruidDataSource druidDataSource) { + druidDataSource.setConnectionErrorRetryAttempts(5); + druidDataSource.setDriverClassName(driverClassName); + druidDataSource.setName(name); + druidDataSource.setInitialSize(initialSize); + druidDataSource.setMinIdle(minIdle); + druidDataSource.setMaxActive(maxActive); + druidDataSource.setMaxWait(maxWait); + druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + druidDataSource.setPoolPreparedStatements(poolPreparedStatements); + druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); + druidDataSource.setMaxOpenPreparedStatements(maxOpenPreparedStatements); + druidDataSource.setValidationQuery(validationQuery); + druidDataSource.setTestWhileIdle(testWhileIdle); + druidDataSource.setTestOnBorrow(testOnBorrow); + druidDataSource.setTestOnReturn(testOnReturn); + return druidDataSource; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java new file mode 100644 index 00000000..e621b784 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CommonWebMvcConfig.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.core.config; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import com.orangeforms.common.core.interceptor.MyRequestArgumentResolver; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyDateUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 所有的项目拦截器、参数解析器、消息对象转换器都在这里集中配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class CommonWebMvcConfig implements WebMvcConfigurer { + + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + // 添加MyRequestBody参数解析器 + argumentResolvers.add(new MyRequestArgumentResolver()); + } + + private HttpMessageConverter responseBodyConverter() { + return new StringHttpMessageConverter(StandardCharsets.UTF_8); + } + + @Bean + public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() { + FastJsonHttpMessageConverter fastConverter = new MyFastJsonHttpMessageConverter(); + List supportedMediaTypes = new ArrayList<>(); + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); + fastConverter.setSupportedMediaTypes(supportedMediaTypes); + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + fastJsonConfig.setSerializerFeatures( + SerializerFeature.PrettyFormat, + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.IgnoreNonFieldGetter); + fastJsonConfig.setDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + fastConverter.setFastJsonConfig(fastJsonConfig); + return fastConverter; + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(responseBodyConverter()); + converters.add(new ByteArrayHttpMessageConverter()); + converters.add(fastJsonHttpMessageConverter()); + } + + public static class MyFastJsonHttpMessageConverter extends FastJsonHttpMessageConverter { + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request == null) { + return super.canWrite(type, clazz, mediaType); + } + if (request.getRequestURI().contains("/v3/api-docs")) { + return false; + } + return super.canWrite(type, clazz, mediaType); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java new file mode 100644 index 00000000..b2bcabe2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/CoreProperties.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * common-core的配置属性类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "common-core") +public class CoreProperties { + + public static final String MYSQL_TYPE = "mysql"; + public static final String POSTGRESQL_TYPE = "postgresql"; + public static final String ORACLE_TYPE = "oracle"; + public static final String DM_TYPE = "dm8"; + public static final String KINGBASE_TYPE = "kingbase"; + public static final String OPENGAUSS_TYPE = "opengauss"; + + /** + * 数据库类型。 + */ + private String databaseType = MYSQL_TYPE; + + /** + * 是否为MySQL。 + * + * @return 是返回true,否则false。 + */ + public boolean isMySql() { + return this.databaseType.equals(MYSQL_TYPE); + } + + /** + * 是否为PostgreSQl。 + * + * @return 是返回true,否则false。 + */ + public boolean isPostgresql() { + return this.databaseType.equals(POSTGRESQL_TYPE); + } + + /** + * 是否为Oracle。 + * + * @return 是返回true,否则false。 + */ + public boolean isOracle() { + return this.databaseType.equals(ORACLE_TYPE); + } + + /** + * 是否为达梦8。 + * + * @return 是返回true,否则false。 + */ + public boolean isDm() { + return this.databaseType.equals(DM_TYPE); + } + + /** + * 是否为人大金仓。 + * + * @return 是返回true,否则false。 + */ + public boolean isKingbase() { + return this.databaseType.equals(KINGBASE_TYPE); + } + + /** + * 是否为华为高斯。 + * + * @return 是返回true,否则false。 + */ + public boolean isOpenGauss() { + return this.databaseType.equals(OPENGAUSS_TYPE); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java new file mode 100644 index 00000000..534443d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceContextHolder.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.core.config; + +/** + * 通过线程本地存储的方式,保存当前数据库操作所需的数据源类型,动态数据源会根据该值,进行动态切换。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class DataSourceContextHolder { + + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源类型。 + * + * @param type 数据源类型 + * @return 原有数据源类型,如果第一次设置则返回null。 + */ + public static Integer setDataSourceType(Integer type) { + Integer datasourceType = CONTEXT_HOLDER.get(); + CONTEXT_HOLDER.set(type); + return datasourceType; + } + + /** + * 获取当前数据库操作执行线程的数据源类型,同时由动态数据源的路由函数调用。 + * + * @return 数据源类型。 + */ + public static Integer getDataSourceType() { + return CONTEXT_HOLDER.get(); + } + + /** + * 清除线程本地变量,以免内存泄漏。 + + * @param originalType 原有的数据源类型,如果该值为null,则情况本地化变量。 + */ + public static void unset(Integer originalType) { + if (originalType == null) { + CONTEXT_HOLDER.remove(); + } else { + CONTEXT_HOLDER.set(originalType); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataSourceContextHolder() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java new file mode 100644 index 00000000..8e03fcc2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DataSourceInfo.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.core.config; + +import lombok.Data; + +/** + * 主要用户动态多数据源使用的配置数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class DataSourceInfo { + /** + * 用于多数据源切换的数据源类型。 + */ + private Integer datasourceType; + /** + * 用户名。 + */ + private String username; + /** + * 密码。 + */ + private String password; + /** + * 数据库主机。 + */ + private String databaseHost; + /** + * 端口号。 + */ + private Integer port; + /** + * 模式名。 + */ + private String schemaName; + /** + * 数据库名称。 + */ + private String databaseName; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java new file mode 100644 index 00000000..1508412d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/DynamicDataSource.java @@ -0,0 +1,170 @@ +package com.orangeforms.common.core.config; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.util.Assert; + +import java.util.*; + +/** + * 动态数据源对象。当存在多个数据连接时使用。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class DynamicDataSource extends AbstractRoutingDataSource { + + @Autowired + private BaseMultiDataSourceConfig baseMultiDataSourceConfig; + @Autowired + private CoreProperties properties; + + private Set dynamicDatasourceTypeSet = new HashSet<>(); + private static final String ASSERT_MSG = "defaultTargetDatasource can't be null."; + + @Override + protected Object determineCurrentLookupKey() { + return DataSourceContextHolder.getDataSourceType(); + } + + /** + * 重新加载动态添加的数据源。既清空之前动态添加的数据源,同时添加参数中的新数据源列表。 + * + * @param dataSourceInfoList 新动态数据源列表。 + */ + public synchronized void reloadAll(List dataSourceInfoList) { + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + dynamicDatasourceTypeSet.forEach(dataSourceMap::remove); + dynamicDatasourceTypeSet.clear(); + for (DataSourceInfo dataSourceInfo : dataSourceInfoList) { + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + } + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 添加动态添加数据源。 + * + * 动态添加数据源。 + */ + public synchronized void addDataSource(DataSourceInfo dataSourceInfo) { + if (dynamicDatasourceTypeSet.contains(dataSourceInfo.getDatasourceType())) { + return; + } + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 添加动态添加数据源列表。 + * + * @param dataSourceInfoList 数据源信息列表。 + */ + public synchronized void addDataSources(List dataSourceInfoList) { + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + for (DataSourceInfo dataSourceInfo : dataSourceInfoList) { + if (!dynamicDatasourceTypeSet.contains(dataSourceInfo.getDatasourceType())) { + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + } + } + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 动态移除数据源。 + * + * @param datasourceType 数据源类型。 + */ + public synchronized void removeDataSource(int datasourceType) { + if (!dynamicDatasourceTypeSet.remove(datasourceType)) { + return; + } + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + dataSourceMap.remove(datasourceType); + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + private DruidDataSource doConvert(DataSourceInfo dataSourceInfo) { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + dataSource.setUsername(dataSourceInfo.getUsername()); + dataSource.setPassword(dataSourceInfo.getPassword()); + StringBuilder urlBuilder = new StringBuilder(256); + String hostAndPort = dataSourceInfo.getDatabaseHost() + ":" + dataSourceInfo.getPort(); + if (properties.isMySql()) { + urlBuilder.append("jdbc:mysql://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()) + .append("?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"); + } else if (properties.isOracle()) { + urlBuilder.append("jdbc:oracle:thin:@") + .append(hostAndPort) + .append(":") + .append(dataSourceInfo.getDatabaseName()); + } else if (properties.isPostgresql()) { + urlBuilder.append("jdbc:postgresql://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()); + if (StrUtil.isBlank(dataSourceInfo.getSchemaName())) { + urlBuilder.append("?currentSchema=public"); + } else { + urlBuilder.append("?currentSchema=").append(dataSourceInfo.getSchemaName()); + } + urlBuilder.append("&TimeZone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8"); + } else if (properties.isDm()) { + urlBuilder.append("jdbc:dm://") + .append(hostAndPort) + .append("?schema=") + .append(dataSourceInfo.getDatabaseName()) + .append("&useJDBCCompliantTimezoneShift=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8"); + } else if (properties.isKingbase()) { + urlBuilder.append("jdbc:kingbase8://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()) + .append("?useJDBCCompliantTimezoneShift=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8"); + } else if (properties.isOpenGauss()) { + urlBuilder.append("jdbc:opengauss://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()); + if (StrUtil.isBlank(dataSourceInfo.getSchemaName())) { + urlBuilder.append("?currentSchema=public"); + } else { + urlBuilder.append("?currentSchema=").append(dataSourceInfo.getSchemaName()); + } + } + dataSource.setUrl(urlBuilder.toString()); + return dataSource; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java new file mode 100644 index 00000000..830199b7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/EncryptConfig.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * 目前用于用户密码加密,UAA接入应用客户端的client_secret加密。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class EncryptConfig { + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/MybatisPlusKeyGeneratorConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/MybatisPlusKeyGeneratorConfig.java new file mode 100644 index 00000000..5aee904d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/MybatisPlusKeyGeneratorConfig.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.config; + +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 仅仅用于Oracle,基于Sequence计算自增字段值的生成器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class MybatisPlusKeyGeneratorConfig { + + @Bean + public IKeyGenerator keyGenerator() { + return new OracleKeyGenerator(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java new file mode 100644 index 00000000..d8deb0ad --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/RestTemplateConfig.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.core.config; + +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * RestTemplate连接池配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class RestTemplateConfig { + private static final int MAX_TOTAL_CONNECTION = 50; + private static final int MAX_CONNECTION_PER_ROUTE = 20; + private static final int CONNECTION_TIMEOUT = 20000; + private static final int READ_TIMEOUT = 30000; + + @Bean + @ConditionalOnMissingBean({RestOperations.class, RestTemplate.class}) + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(createFactory()); + List> messageConverters = restTemplate.getMessageConverters(); + messageConverters.removeIf( + c -> c instanceof StringHttpMessageConverter || c instanceof MappingJackson2HttpMessageConverter); + messageConverters.add(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); + messageConverters.add(new FastJsonHttpMessageConverter()); + restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + public void handleError(ClientHttpResponse response) throws IOException { + // 防止400+和500等错误被直接抛出异常,这里避开了缺省处理方式,所有的错误均交给业务代码处理。 + } + }); + return restTemplate; + } + + private ClientHttpRequestFactory createFactory() { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(MAX_TOTAL_CONNECTION); + connectionManager.setDefaultMaxPerRoute(MAX_CONNECTION_PER_ROUTE); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(CONNECTION_TIMEOUT, TimeUnit.MICROSECONDS) + .setResponseTimeout(READ_TIMEOUT, TimeUnit.MICROSECONDS) + .build(); + HttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(connectionManager) + .build(); + return new HttpComponentsClientHttpRequestFactory(httpClient); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java new file mode 100644 index 00000000..90ed08fd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/config/TomcatConfig.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.core.config; + +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * tomcat配置对象。当前配置禁用了PUT和DELETE方法,防止渗透攻击。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class TomcatConfig { + + @Bean + public TomcatServletWebServerFactory servletContainer() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.addContextCustomizers(context -> { + SecurityConstraint securityConstraint = new SecurityConstraint(); + securityConstraint.setUserConstraint("CONFIDENTIAL"); + SecurityCollection collection = new SecurityCollection(); + collection.addPattern("/*"); + collection.addMethod("HEAD"); + collection.addMethod("PUT"); + collection.addMethod("PATCH"); + collection.addMethod("DELETE"); + collection.addMethod("TRACE"); + collection.addMethod("COPY"); + collection.addMethod("SEARCH"); + collection.addMethod("PROPFIND"); + securityConstraint.addCollection(collection); + context.addConstraint(securityConstraint); + }); + return factory; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java new file mode 100644 index 00000000..d0368de0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AggregationType.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 聚合计算的常量类型对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class AggregationType { + + /** + * sum 计数 + */ + public static final int SUM = 0; + /** + * count 汇总 + */ + public static final int COUNT = 1; + /** + * average 平均值 + */ + public static final int AVG = 2; + /** + * min 最小值 + */ + public static final int MIN = 3; + /** + * max 最大值 + */ + public static final int MAX = 4; + + private static final Map DICT_MAP = new HashMap<>(5); + static { + DICT_MAP.put(SUM, "累计总和"); + DICT_MAP.put(COUNT, "数量总和"); + DICT_MAP.put(AVG, "平均值"); + DICT_MAP.put(MIN, "最小值"); + DICT_MAP.put(MAX, "最大值"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 获取与SQL对应的聚合函数字符串名称。 + * + * @return 聚合函数名称。 + */ + public static String getAggregationFunction(Integer aggregationType) { + switch (aggregationType) { + case COUNT: + return "COUNT"; + case AVG: + return "AVG"; + case SUM: + return "SUM"; + case MAX: + return "MAX"; + case MIN: + return "MIN"; + default: + throw new IllegalArgumentException("无效的聚合类型!"); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AggregationType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java new file mode 100644 index 00000000..edad8271 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/AppDeviceType.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * App 登录的设备类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class AppDeviceType { + + /** + * 移动端 (如果不考虑区分android或ios的,可以使用该值) + */ + public static final int MOBILE = 0; + /** + * android + */ + public static final int ANDROID = 1; + /** + * iOS + */ + public static final int IOS = 2; + /** + * 微信公众号和小程序 + */ + public static final int WEIXIN = 3; + /** + * PC WEB + */ + public static final int WEB = 4; + + private static final Map DICT_MAP = new HashMap<>(5); + static { + DICT_MAP.put(MOBILE, "Mobile"); + DICT_MAP.put(ANDROID, "Android"); + DICT_MAP.put(IOS, "iOS"); + DICT_MAP.put(WEIXIN, "Wechat"); + DICT_MAP.put(WEB, "WEB"); + } + + /** + * 根据设备类型返回设备名称。 + * + * @param deviceType 设备类型。 + * @return 设备名称。 + */ + public static String getDeviceTypeName(int deviceType) { + return DICT_MAP.get(deviceType); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AppDeviceType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java new file mode 100644 index 00000000..25fce820 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ApplicationConstant.java @@ -0,0 +1,161 @@ +package com.orangeforms.common.core.constant; + +import java.util.regex.Pattern; + +/** + * 应用程序的常量声明对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class ApplicationConstant { + + /** + * 适用于所有类型的字典格式数据。该常量为字典的键字段。 + */ + public static final String DICT_ID = "id"; + /** + * 适用于所有类型的字典格式数据。该常量为字典的名称字段。 + */ + public static final String DICT_NAME = "name"; + /** + * 适用于所有类型的字典格式数据。该常量为字典的键父字段。 + */ + public static final String PARENT_ID = "parentId"; + /** + * 数据同步使用的缺省消息队列主题名称。 + */ + public static final String DEFAULT_DATA_SYNC_TOPIC = "OrangeFormsOpen"; + /** + * 全量数据同步中,新增数据对象的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_DATA_KEY = "data"; + /** + * 全量数据同步中,原有数据对象的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_OLD_DATA_KEY = "oldData"; + /** + * 全量数据同步中,数据对象主键的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_ID_KEY = "id"; + /** + * 为字典表数据缓存时,缓存名称的固定后缀。 + */ + public static final String DICT_CACHE_NAME_SUFFIX = "-DICT"; + /** + * 为树形字典表数据缓存时,缓存名称的固定后缀。 + */ + public static final String TREE_DICT_CACHE_NAME_SUFFIX = "-TREE-DICT"; + /** + * 图片文件上传的父目录。 + */ + public static final String UPLOAD_IMAGE_PARENT_PATH = "image"; + /** + * 附件文件上传的父目录。 + */ + public static final String UPLOAD_ATTACHMENT_PARENT_PATH = "attachment"; + /** + * CSV文件扩展名。 + */ + public static final String CSV_EXT = "csv"; + /** + * XLSX文件扩展名。 + */ + public static final String XLSX_EXT = "xlsx"; + /** + * 统计分类计算时,按天聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String DAY_AGGREGATION = "day"; + /** + * 统计分类计算时,按月聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String MONTH_AGGREGATION = "month"; + /** + * 统计分类计算时,按年聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String YEAR_AGGREGATION = "year"; + /** + * 请求头跟踪id名。 + */ + public static final String HTTP_HEADER_TRACE_ID = "traceId"; + /** + * 请求头菜单Id。 + */ + public static final String HTTP_HEADER_MENU_ID = "MenuId"; + /** + * 数据权限中,标记所有菜单的Id值。 + */ + public static final String DATA_PERM_ALL_MENU_ID = "AllMenuId"; + /** + * 请求头中记录的原始请求URL。 + */ + public static final String HTTP_HEADER_ORIGINAL_REQUEST_URL = "MY_ORIGINAL_REQUEST_URL"; + /** + * 免登录验证接口的请求头key。 + */ + public static final String HTTP_HEADER_DONT_AUTH = "DONT_AUTH"; + /** + * 系统服务内部调用时,可使用该HEAD,以便和外部调用加以区分,便于监控和流量分析。 + */ + public static final String HTTP_HEADER_INTERNAL_TOKEN = "INTERNAL_AUTH_TOKEN"; + /** + * 操作日志的数据源类型。 + */ + public static final int OPERATION_LOG_DATASOURCE_TYPE = 1000; + /** + * 在线表单的数据源类型。 + */ + public static final int COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE = 1010; + /** + * 报表模块的数据源类型。 + */ + public static final int COMMON_REPORT_DATASOURCE_TYPE = 1020; + /** + * 全局编码字典的数据源类型。 + */ + public static final int COMMON_GLOBAL_DICT_TYPE = 1050; + /** + * 租户管理所对应的数据源常量值。 + */ + public static final int TENANT_ADMIN_DATASOURCE_TYPE = 1100; + /** + * 租户业务默认数据库(系统搭建时的第一个租户数据库)所对应的数据源常量值。 + */ + public static final int TENANT_BUSINESS_DATASOURCE_TYPE = 1120; + /** + * 租户通用数据所对应的数据源常量值,如全局编码字典、在线表单、流程和报表等内置表数据。 + */ + public static final int TENANT_COMMON_DATASOURCE_TYPE = 1130; + /** + * 租户动态数据源主题(Redis)。 + */ + public static final String TENANT_DYNAMIC_DATASOURCE_TOPIC = "TenantDynamicDatasoruce"; + /** + * 租户基础数据同步(RocketMQ),如upms、全局编码字典、在线表单、流程、报表等。 + */ + public static final String TENANT_DATASYNC_TOPIC = "TenantSync"; + /** + * 租户管理的应用名。 + */ + public static final String TENANT_ADMIN_APP_NAME = "tenant-admin"; + /** + * 重要说明:该值为项目生成后的缺省密钥,仅为使用户可以快速上手并跑通流程。 + * 在实际的应用中,一定要为不同的项目或服务,自行生成公钥和私钥,并将 PRIVATE_KEY 的引用改为服务的配置项。 + * 密钥的生成方式,可通过执行common.core.util.RsaUtil类的main函数动态生成。 + */ + public static final String PRIVATE_KEY = + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKkLhAydtOtA4WuIkkIIUVaGWu4ElOEAQF9GTulHHWOwCHI1UvcKolvS1G+mdsKcmGtEAQ92AUde/kDRGu8Wn7kLDtCgUfo72soHz7Qfv5pVB4ohMxQd/9cxeKjKbDoirhB9Z3xGF20zUozp4ZPLxpTtI7azr0xzUtd5+D/HfLDrAgMBAAECgYEApESZhDz4YyeAJiPnpJ06lS8oS2VOWzsIUs0av5uoloeoHXtt7Lx7u2kroHeNrl3Hy2yg7ypH4dgQkGHin3VHrVAgjG3TxhgBXIqqntzzk2AGJKBeIIkRX86uTvtKZyp3flUgcwcGmpepAHS1V1DPY3aVYvbcqAmoL6DX6VYN0NECQQDQUitMdC76lEtAr5/ywS0nrZJDo6U7eQ7ywx/eiJ+YmrSye8oorlAj1VBWG+Cl6jdHOHtTQyYv/tu71fjzQiJTAkEAz7wb47/vcSUpNWQxItFpXz0o6rbJh71xmShn1AKP7XptOVZGlW9QRYEzHabV9m/DHqI00cMGhHrWZAhCiTkUCQJAFsJjaJ7o4weAkTieyO7B+CvGZw1h5/V55Jvcx3s1tH5yb22G0Jr6tm9/r2isSnQkReutzZLwgR3e886UvD7lcQJAAUcD2OOuQkDbPwPNtYwaHMbQgJj9JkOI9kskUE5vuiMdltOr/XFAyhygRtdmy2wmhAK1VnDfkmL6/IR8fEGImQJABOB0KCalb0M8CPnqqHzozrD8gPObnIIr4aVvLIPATN2g7MM2N6F7JbI4RZFiKa92LV6bhQCY8OvHi5K2cgFpbw=="; + /** + * SQL注入检测的正则对象。 + */ + @SuppressWarnings("all") + public static final Pattern SQL_INJECT_PATTERN = + Pattern.compile("(.*\\=.*\\-\\-.*)|(.*(\\+).*)|(.*\\w+(%|\\$|#|&)\\w+.*)|(.*\\|\\|.*)|(.*\\s+(and|or)\\s+.*)" + + "|(.*\\b(select|update|union|and|or|delete|insert|trancate|char|substr|ascii|declare|exec|count|master|into|drop|execute|sleep|extractvalue|updatexml|substring|database|concat|rand)\\b.*)"); + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ApplicationConstant() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java new file mode 100644 index 00000000..772d0597 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DataPermRuleType.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据权限规则类型常量类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DataPermRuleType { + + /** + * 查看全部。 + */ + public static final int TYPE_ALL = 0; + + /** + * 仅查看当前用户。 + */ + public static final int TYPE_USER_ONLY = 1; + + /** + * 仅查看当前部门。 + */ + public static final int TYPE_DEPT_ONLY = 2; + + /** + * 所在部门及子部门。 + */ + public static final int TYPE_DEPT_AND_CHILD_DEPT = 3; + + /** + * 多部门及子部门。 + */ + public static final int TYPE_MULTI_DEPT_AND_CHILD_DEPT = 4; + + /** + * 自定义部门列表。 + */ + public static final int TYPE_CUSTOM_DEPT_LIST = 5; + + /** + * 本部门所有用户。 + */ + public static final int TYPE_DEPT_USERS = 6; + + /** + * 本部门及子部门所有用户。 + */ + public static final int TYPE_DEPT_AND_CHILD_DEPT_USERS = 7; + + private static final Map DICT_MAP = new HashMap<>(6); + static { + DICT_MAP.put(TYPE_ALL, "查看全部"); + DICT_MAP.put(TYPE_USER_ONLY, "仅查看当前用户"); + DICT_MAP.put(TYPE_DEPT_ONLY, "仅查看所在部门"); + DICT_MAP.put(TYPE_DEPT_AND_CHILD_DEPT, "所在部门及子部门"); + DICT_MAP.put(TYPE_MULTI_DEPT_AND_CHILD_DEPT, "多部门及子部门"); + DICT_MAP.put(TYPE_CUSTOM_DEPT_LIST, "自定义部门列表"); + DICT_MAP.put(TYPE_DEPT_USERS, "本部门所有用户"); + DICT_MAP.put(TYPE_DEPT_AND_CHILD_DEPT_USERS, "本部门及子部门所有用户"); + } + + /** + * 判断参数是否为当前常量字典的合法取值范围。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataPermRuleType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java new file mode 100644 index 00000000..5d294431 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/DictType.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字典类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DictType { + + /** + * 数据表字典。 + */ + public static final int TABLE = 1; + /** + * URL字典。 + */ + public static final int URL = 5; + /** + * 常量字典。 + */ + public static final int CONST = 10; + /** + * 自定义字典。 + */ + public static final int CUSTOM = 15; + /** + * 全局编码字典。 + */ + public static final int GLOBAL_DICT = 20; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(TABLE, "数据表字典"); + DICT_MAP.put(URL, "URL字典"); + DICT_MAP.put(CONST, "静态字典"); + DICT_MAP.put(CUSTOM, "自定义字典"); + DICT_MAP.put(GLOBAL_DICT, "全局编码字典"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DictType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java new file mode 100644 index 00000000..423ba928 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ErrorCodeEnum.java @@ -0,0 +1,88 @@ +package com.orangeforms.common.core.constant; + +/** + * 返回应答中的错误代码和错误信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +public enum ErrorCodeEnum { + + /** + * 没有错误 + */ + NO_ERROR("没有错误"), + /** + * 未处理的异常! + */ + UNHANDLED_EXCEPTION("未处理的异常!"), + + ARGUMENT_NULL_EXIST("数据验证失败,接口调用参数存在空值,请核对!"), + ARGUMENT_PK_ID_NULL("数据验证失败,接口调用主键Id参数为空,请核对!"), + INVALID_ARGUMENT_FORMAT("数据验证失败,不合法的参数格式,请核对!"), + INVALID_STATUS_ARGUMENT("数据验证失败,无效的状态参数值,请核对!"), + UPLOAD_FAILED("数据验证失败,数据上传失败!"), + INVALID_UPLOAD_FIELD("数据验证失败,该字段不支持数据上传!"), + INVALID_UPLOAD_STORE_TYPE("数据验证失败,并不支持上传存储类型!"), + INVALID_UPLOAD_FILE_ARGUMENT("数据验证失败,上传文件参数错误,请核对!"), + INVALID_UPLOAD_FILE_FORMAT("无效的上传文件格式!"), + INVALID_UPLOAD_FILE_IOERROR("上传文件写入失败,请联系管理员!"), + UNAUTHORIZED_LOGIN("当前用户尚未登录或登录已超时,请重新登录!"), + UNAUTHORIZED_USER_PERMISSION("权限验证失败,当前用户不能访问该接口,请核对!"), + NO_ACCESS_PERMISSION("当前用户没有访问权限,请核对!"), + NO_OPERATION_PERMISSION("当前用户没有操作权限,请核对!"), + + PASSWORD_ERR("密码错误,请重试!"), + INVALID_USERNAME_PASSWORD("用户名或密码错误,请重试!"), + INVALID_ACCESS_TOKEN("无效的用户访问令牌!"), + INVALID_USER_STATUS("用户状态错误,请刷新后重试!"), + INVALID_TENANT_CODE("指定的租户编码并不存在,请刷新后重试!"), + INVALID_TENANT_STATUS("当前租户为不可用状态,请刷新后重试!"), + INVALID_USER_TENANT("当前用户并不属于当前租户,请刷新后重试!"), + + HAS_CHILDREN_DATA("数据验证失败,子数据存在,请刷新后重试!"), + DATA_VALIDATED_FAILED("数据验证失败,请核对!"), + UPLOAD_FILE_FAILED("文件上传失败,请联系管理员!"), + DATA_SAVE_FAILED("数据保存失败,请联系管理员!"), + DATA_ACCESS_FAILED("数据访问失败,请联系管理员!"), + DATA_PERM_ACCESS_FAILED("数据访问失败,您没有该页面的数据访问权限!"), + DUPLICATED_UNIQUE_KEY("数据保存失败,存在重复数据,请核对!"), + DATA_NOT_EXIST("数据不存在,请刷新后重试!"), + DATA_PARENT_LEVEL_ID_NOT_EXIST("数据验证失败,父级别关联Id不存在,请刷新后重试!"), + DATA_PARENT_ID_NOT_EXIST("数据验证失败,ParentId不存在,请核对!"), + INVALID_RELATED_RECORD_ID("数据验证失败,关联数据并不存在,请刷新后重试!"), + INVALID_DATA_MODEL("数据验证失败,无效的数据实体对象!"), + INVALID_DATA_FIELD("数据验证失败,无效的数据实体对象字段!"), + INVALID_CLASS_FIELD("数据验证失败,无效的类对象字段!"), + SERVER_INTERNAL_ERROR("服务器内部错误,请联系管理员!"), + REDIS_CACHE_ACCESS_TIMEOUT("Redis缓存数据访问超时,请刷新后重试!"), + REDIS_CACHE_ACCESS_STATE_ERROR("Redis缓存数据访问状态错误,请刷新后重试!"), + FAILED_TO_INVOKE_THIRDPARTY_URL("调用第三方接口失败!"), + + FLOW_WORK_ORDER_EXIST("该业务数据Id存在尚未完成审批的流程实例,同一业务数据主键不能同时重复提交审批!"); + + // 下面的枚举值为特定枚举值,即开发者可以根据自己的项目需求定义更多的非通用枚举值 + + /** + * 构造函数。 + * + * @param errorMessage 错误消息。 + */ + ErrorCodeEnum(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * 错误信息。 + */ + private final String errorMessage; + + /** + * 获取错误信息。 + * + * @return 错误信息。 + */ + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java new file mode 100644 index 00000000..db0e1752 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FieldFilterType.java @@ -0,0 +1,127 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FieldFilterType { + /** + * 等于过滤。 + */ + public static final int EQUAL = 0; + /** + * 不等于过滤。 + */ + public static final int NOT_EQUAL = 1; + /** + * 大于等于。 + */ + public static final int GE = 2; + /** + * 大于。 + */ + public static final int GT = 3; + /** + * 小于等于。 + */ + public static final int LE = 4; + /** + * 小于。 + */ + public static final int LT = 5; + /** + * 模糊查询。 + */ + public static final int LIKE = 6; + /** + * IN列表过滤。 + */ + public static final int IN = 7; + /** + * NOT IN列表过滤。 + */ + public static final int NOT_IN = 8; + /** + * 范围过滤。 + */ + public static final int BETWEEN = 9; + /** + * 不为空。 + */ + public static final int IS_NOT_NULL = 100; + /** + * 为空。 + */ + public static final int IS_NULL = 101; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(EQUAL, " = "); + DICT_MAP.put(NOT_EQUAL, " <> "); + DICT_MAP.put(GE, " >= "); + DICT_MAP.put(GT, " > "); + DICT_MAP.put(LE, " <= "); + DICT_MAP.put(LT, " < "); + DICT_MAP.put(LIKE, " LIKE "); + DICT_MAP.put(IN, " IN "); + DICT_MAP.put(NOT_IN, " NOT IN "); + DICT_MAP.put(BETWEEN, " BETWEEN "); + DICT_MAP.put(IS_NOT_NULL, " IS NOT NULL "); + DICT_MAP.put(IS_NULL, " IS NULL "); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 获取显示名。 + * @param value 常量值。 + * @return 常量值对应的显示名。 + */ + public static String getName(Integer value) { + return DICT_MAP.get(value); + } + + /** + * 不支持日期型字段的过滤类型。 + * + * @param filterType 过滤类型。 + * @return 不支持返回true,否则false。 + */ + public static boolean unsupportDateFilterType(int filterType) { + return filterType == FieldFilterType.IN + || filterType == FieldFilterType.NOT_IN + || filterType == FieldFilterType.NOT_EQUAL + || filterType == FieldFilterType.LIKE; + } + + /** + * 支持多过滤值的过滤类型。 + * + * @param filterType 过滤类型。 + * @return 支持返回true,否则false。 + */ + public static boolean supportMultiValueFilterType(int filterType) { + return filterType == FieldFilterType.IN + || filterType == FieldFilterType.NOT_IN + || filterType == FieldFilterType.BETWEEN; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldFilterType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java new file mode 100644 index 00000000..dda91b2e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/FilterParamType.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤参数类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FilterParamType { + + /** + * 整数数值型。 + */ + public static final int LONG = 0; + /** + * 浮点型。 + */ + public static final int FLOAT = 1; + /** + * 字符型。 + */ + public static final int STRING = 2; + /** + * 日期型。 + */ + public static final int DATE = 3; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(LONG, "整数数值型"); + DICT_MAP.put(FLOAT, "浮点型"); + DICT_MAP.put(STRING, "字符型"); + DICT_MAP.put(DATE, "日期型"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FilterParamType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java new file mode 100644 index 00000000..a7ed6ba3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/GlobalDeletedFlag.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.core.constant; + +/** + * 数据记录逻辑删除标记常量。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class GlobalDeletedFlag { + + /** + * 表示数据表记录已经删除 + */ + public static final int DELETED = -1; + /** + * 数据记录正常 + */ + public static final int NORMAL = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalDeletedFlag() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java new file mode 100644 index 00000000..d242e26c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/MaskFieldTypeEnum.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.core.constant; + +/** + * 字段脱敏类型枚举。。 + * + * @author Jerry + * @date 2024-07-02 + */ +public enum MaskFieldTypeEnum { + + /** + * 自定义实现。 + */ + CUSTOM, + /** + * 姓名。 + */ + NAME, + /** + * 移动电话。 + */ + MOBILE_PHONE, + /** + * 座机电话。 + */ + FIXED_PHONE, + /** + * 身份证。 + */ + ID_CARD, + /** + * 银行卡号。 + */ + BANK_CARD, + /** + * 汽车牌照号。 + */ + CAR_LICENSE, + /** + * 邮件。 + */ + EMAIL, + /** + * 固定长度的前缀和后缀不被掩码。 + */ + NO_MASK_PREFIX_SUFFIX, +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java new file mode 100644 index 00000000..660b606c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/ObjectFieldType.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.constant; + +/** + * 对应于数据表字段中的类型,我们需要统一映射到Java实体对象字段的类型。 + * 该类是描述Java实体对象字段类型的常量类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class ObjectFieldType { + + public static final String LONG = "Long"; + public static final String INTEGER = "Integer"; + public static final String DOUBLE = "Double"; + public static final String BIG_DECIMAL = "BigDecimal"; + public static final String BOOLEAN = "Boolean"; + public static final String STRING = "String"; + public static final String DATE = "Date"; + public static final String BYTE_ARRAY = "byte[]"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ObjectFieldType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java new file mode 100644 index 00000000..d966cf6d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/constant/UserFilterGroup.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.core.constant; + +/** + * 用户分组过滤常量。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class UserFilterGroup { + + public static final String USER = "USER_GROUP"; + public static final String ROLE = "ROLE_GROUP"; + public static final String DEPT = "DEPT_GROUP"; + public static final String POST = "POST_GROUP"; + public static final String DEPT_POST = "DEPT_POST_GROUP"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private UserFilterGroup() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java new file mode 100644 index 00000000..66053ad5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/DataValidationException.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.exception; + +/** + * 数据验证失败的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class DataValidationException extends RuntimeException { + + /** + * 构造函数。 + */ + public DataValidationException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public DataValidationException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java new file mode 100644 index 00000000..762eac91 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidClassFieldException.java @@ -0,0 +1,30 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的类对象字段的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidClassFieldException extends RuntimeException { + + private final String className; + private final String fieldName; + + /** + * 构造函数。 + * + * @param className 对象名。 + * @param fieldName 字段名。 + */ + public InvalidClassFieldException(String className, String fieldName) { + super("Invalid FieldName [" + fieldName + "] in Class [" + className + "]."); + this.className = className; + this.fieldName = fieldName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java new file mode 100644 index 00000000..2c5d249e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataFieldException.java @@ -0,0 +1,30 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的实体对象字段的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDataFieldException extends RuntimeException { + + private final String modelName; + private final String fieldName; + + /** + * 构造函数。 + * + * @param modelName 实体对象名。 + * @param fieldName 字段名。 + */ + public InvalidDataFieldException(String modelName, String fieldName) { + super("Invalid FieldName [" + fieldName + "] in Model Class [" + modelName + "]."); + this.modelName = modelName; + this.fieldName = fieldName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java new file mode 100644 index 00000000..b17abb8e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDataModelException.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的实体对象的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDataModelException extends RuntimeException { + + private final String modelName; + + /** + * 构造函数。 + * + * @param modelName 实体对象名。 + */ + public InvalidDataModelException(String modelName) { + super("Invalid Model Class [" + modelName + "]."); + this.modelName = modelName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java new file mode 100644 index 00000000..b7589219 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidDblinkTypeException.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的数据库链接类型自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDblinkTypeException extends RuntimeException { + + /** + * 构造函数。 + * + * @param dblinkType 数据库链接类型。 + */ + public InvalidDblinkTypeException(int dblinkType) { + super("Invalid Dblink Type [" + dblinkType + "]."); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java new file mode 100644 index 00000000..9b197625 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/InvalidRedisModeException.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的Redis模式的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidRedisModeException extends RuntimeException { + + private final String mode; + + /** + * 构造函数。 + * + * @param mode 错误的模式。 + */ + public InvalidRedisModeException(String mode) { + super("Invalid Redis Mode [" + mode + "], only supports [single/cluster/sentinel/master_slave]"); + this.mode = mode; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java new file mode 100644 index 00000000..b47dd010 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MapCacheAccessException.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.core.exception; + +/** + * 内存缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MapCacheAccessException extends RuntimeException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param cause 原始异常。 + */ + public MapCacheAccessException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java new file mode 100644 index 00000000..82d8f4ae --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/MyRuntimeException.java @@ -0,0 +1,46 @@ +package com.orangeforms.common.core.exception; + +/** + * 自定义的运行时异常,在需要抛出运行时异常时,可使用该异常。 + * NOTE:主要是为了避免SonarQube进行代码质量扫描时,给出警告。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyRuntimeException extends RuntimeException { + + /** + * 构造函数。 + */ + public MyRuntimeException() { + + } + + /** + * 构造函数。 + * + * @param throwable 引发异常对象。 + */ + public MyRuntimeException(Throwable throwable) { + super(throwable); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public MyRuntimeException(String msg) { + super(msg); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param throwable 引发异常对象。 + */ + public MyRuntimeException(String msg, Throwable throwable) { + super(msg, throwable); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java new file mode 100644 index 00000000..0d9dd3d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataAffectException.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.exception; + +/** + * 没有数据被修改的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class NoDataAffectException extends RuntimeException { + + /** + * 构造函数。 + */ + public NoDataAffectException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public NoDataAffectException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java new file mode 100644 index 00000000..2e18d311 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/NoDataPermException.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.core.exception; + +/** + * 没有数据访问权限的自定义异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class NoDataPermException extends RuntimeException { + + /** + * 构造函数。 + */ + public NoDataPermException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public NoDataPermException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java new file mode 100644 index 00000000..b0dfe017 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/exception/RedisCacheAccessException.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.core.exception; + +/** + * Redis缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class RedisCacheAccessException extends RuntimeException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param cause 原始异常。 + */ + public RedisCacheAccessException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java new file mode 100644 index 00000000..08c198ad --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/interceptor/MyRequestArgumentResolver.java @@ -0,0 +1,227 @@ +package com.orangeforms.common.core.interceptor; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import org.apache.commons.io.IOUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.util.*; + +/** + * MyRequestBody解析器 + * 解决的问题: + * 1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收; + * 2、多个对象需要封装到一个对象里才可以用@RequestBody接收。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyRequestArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String JSONBODY_ATTRIBUTE = "MY_REQUEST_BODY_ATTRIBUTE_XX"; + + private static final Set> CLASS_SET = new HashSet<>(); + + static { + CLASS_SET.add(Integer.class); + CLASS_SET.add(Long.class); + CLASS_SET.add(Short.class); + CLASS_SET.add(Float.class); + CLASS_SET.add(Double.class); + CLASS_SET.add(Boolean.class); + CLASS_SET.add(Byte.class); + CLASS_SET.add(BigDecimal.class); + CLASS_SET.add(Character.class); + CLASS_SET.add(Date.class); + } + + /** + * 设置支持的方法参数类型。 + * + * @param parameter 方法参数。 + * @return 支持的类型。 + */ + @Override + public boolean supportsParameter(@NonNull MethodParameter parameter) { + return parameter.hasParameterAnnotation(MyRequestBody.class); + } + + /** + * 参数解析,利用fastjson。 + * 注意:非基本类型返回null会报空指针异常,要通过反射或者JSON工具类创建一个空对象。 + */ + @Override + public Object resolveArgument( + @NonNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + @NonNull NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.notNull(servletRequest, "HttpServletRequest can't be NULL."); + String contentType = servletRequest.getContentType(); + if (!HttpMethod.POST.name().equals(servletRequest.getMethod())) { + throw new IllegalArgumentException("Only POST method can be applied @MyRequestBody annotation!"); + } + if (!StrUtil.containsIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE)) { + throw new IllegalArgumentException( + "Only application/json Content-Type can be applied @MyRequestBody annotation!"); + } + // 根据@MyRequestBody注解value作为json解析的key + MyRequestBody parameterAnnotation = parameter.getParameterAnnotation(MyRequestBody.class); + Assert.notNull(parameterAnnotation, "parameterAnnotation can't be NULL"); + JSONObject jsonObject = getRequestBody(webRequest); + if (jsonObject == null) { + if (parameterAnnotation.required()) { + throw new IllegalArgumentException("Request Body is EMPTY!"); + } + return null; + } + String key = parameterAnnotation.value(); + if (StrUtil.isBlank(key)) { + key = parameter.getParameterName(); + } + Object value = jsonObject.get(key); + if (value == null) { + if (parameterAnnotation.required()) { + throw new IllegalArgumentException(String.format("Required parameter %s is not present!", key)); + } + return null; + } + // 获取参数类型。 + Class parameterType = parameter.getParameterType(); + // 基本类型 + if (parameterType.isPrimitive()) { + return parsePrimitive(parameterType.getName(), value); + } + // 基本类型包装类 + if (isBasicDataTypes(parameterType)) { + return parseBasicTypeWrapper(parameterType, value); + } else if (parameterType == String.class) { + // 字符串类型 + return value.toString(); + } + // 对象类型 + if (!(value instanceof JSONArray)) { + // 其他复杂对象 + return JSON.toJavaObject((JSONObject) value, parameterType); + } + if (parameter.getGenericParameterType() instanceof ParameterizedType) { + return ((JSONArray) value).toJavaObject(parameter.getGenericParameterType()); + } + // 非参数化的集合类型 + return JSON.parseObject(value.toString(), parameterType); + } + + private Object parsePrimitive(String parameterTypeName, Object value) { + final String booleanTypeName = "boolean"; + if (booleanTypeName.equals(parameterTypeName)) { + return Boolean.valueOf(value.toString()); + } + final String intTypeName = "int"; + if (intTypeName.equals(parameterTypeName)) { + return Integer.valueOf(value.toString()); + } + final String charTypeName = "char"; + if (charTypeName.equals(parameterTypeName)) { + return value.toString().charAt(0); + } + final String shortTypeName = "short"; + if (shortTypeName.equals(parameterTypeName)) { + return Short.valueOf(value.toString()); + } + final String longTypeName = "long"; + if (longTypeName.equals(parameterTypeName)) { + return Long.valueOf(value.toString()); + } + final String floatTypeName = "float"; + if (floatTypeName.equals(parameterTypeName)) { + return Float.valueOf(value.toString()); + } + final String doubleTypeName = "double"; + if (doubleTypeName.equals(parameterTypeName)) { + return Double.valueOf(value.toString()); + } + final String byteTypeName = "byte"; + if (byteTypeName.equals(parameterTypeName)) { + return Byte.valueOf(value.toString()); + } + return null; + } + + private Object parseBasicTypeWrapper(Class parameterType, Object value) { + if (Number.class.isAssignableFrom(parameterType)) { + return this.parseNumberType(parameterType, value); + } else if (parameterType == Boolean.class) { + return value; + } else if (parameterType == Character.class) { + return value.toString().charAt(0); + } else if (parameterType == Date.class) { + return Convert.toDate(value); + } + return null; + } + + private Object parseNumberType(Class parameterType, Object value) { + if (value instanceof String) { + return Convert.convert(parameterType, value); + } + Number number = (Number) value; + if (parameterType == Integer.class) { + return number.intValue(); + } else if (parameterType == Short.class) { + return number.shortValue(); + } else if (parameterType == Long.class) { + return number.longValue(); + } else if (parameterType == Float.class) { + return number.floatValue(); + } else if (parameterType == Double.class) { + return number.doubleValue(); + } else if (parameterType == Byte.class) { + return number.byteValue(); + } else if (parameterType == BigDecimal.class) { + if (value instanceof Double || value instanceof Float) { + return BigDecimal.valueOf(number.doubleValue()); + } else { + return BigDecimal.valueOf(number.longValue()); + } + } + return null; + } + + private boolean isBasicDataTypes(Class clazz) { + return CLASS_SET.contains(clazz); + } + + private JSONObject getRequestBody(NativeWebRequest webRequest) throws IOException { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.notNull(servletRequest, "servletRequest can't be NULL"); + // 有就直接获取 + JSONObject jsonObject = (JSONObject) webRequest.getAttribute(JSONBODY_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + // 没有就从请求中读取 + if (jsonObject == null) { + String jsonBody = IOUtils.toString(servletRequest.getReader()); + jsonObject = JSON.parseObject(jsonBody); + if (jsonObject != null) { + webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonObject, RequestAttributes.SCOPE_REQUEST); + } + } + return jsonObject; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java new file mode 100644 index 00000000..d2c37fb1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/listener/LoadServiceRelationListener.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.core.listener; + +import com.orangeforms.common.core.base.service.BaseService; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 应用程序启动后的事件监听对象。主要负责加载Model之间的字典关联和一对一关联所对应的Service结构关系。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class LoadServiceRelationListener implements ApplicationListener { + + @SuppressWarnings("all") + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + Map serviceMap = + applicationReadyEvent.getApplicationContext().getBeansOfType(BaseService.class); + for (Map.Entry e : serviceMap.entrySet()) { + e.getValue().loadRelationStruct(); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java new file mode 100644 index 00000000..70e09f76 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/CallResult.java @@ -0,0 +1,103 @@ +package com.orangeforms.common.core.object; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +/** + * 业务方法调用结果对象。可以同时返回具体的错误和JSON类型的数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class CallResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final CallResult OK = new CallResult(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误信息描述。 + */ + private String errorMessage = null; + /** + * 在验证同时,仍然需要附加的关联数据对象。 + */ + private JSONObject data; + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static CallResult create(String errorMessage) { + return errorMessage == null ? ok() : error(errorMessage); + } + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @param data 附带的数据对象。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static CallResult create(String errorMessage, JSONObject data) { + return errorMessage == null ? ok(data) : error(errorMessage); + } + + /** + * 创建表示验证成功的对象实例。 + * + * @return 验证成功对象实例。 + */ + public static CallResult ok() { + return OK; + } + + /** + * 创建表示验证成功的对象实例。 + * + * @param data 附带的数据对象。 + * @return 验证成功对象实例。 + */ + public static CallResult ok(JSONObject data) { + CallResult result = new CallResult(); + result.data = data; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @return 验证失败对象实例。 + */ + public static CallResult error(String errorMessage) { + CallResult result = new CallResult(); + result.success = false; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @param data 附带的数据对象。 + * @return 验证失败对象实例。 + */ + public static CallResult error(String errorMessage, T data) { + CallResult result = new CallResult(); + result.success = false; + result.errorMessage = errorMessage; + JSONObject jsonObject = new JSONObject(); + jsonObject.put("errorData", data); + result.data = jsonObject; + return result; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java new file mode 100644 index 00000000..c3422da4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ColumnEncodedRule.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +/** + * 编码字段的编码规则。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class ColumnEncodedRule { + + /** + * 是否显示是计算并回显。 + */ + private Boolean calculateWhenView; + + /** + * 前缀。 + */ + private String prefix; + + /** + * 精确到DAYS/HOURS/MINUTES/SECONDS + */ + private String precisionTo; + + /** + * 中缀。 + */ + private String middle; + + /** + * 流水序号的字符宽度,不足的前面补0。 + */ + private Integer idWidth; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java new file mode 100644 index 00000000..e063b9ab --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ConstDictInfo.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +import java.util.List; + +/** + * 常量字典的数据结构。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class ConstDictInfo { + + private List dictData; + + @Data + public static class ConstDictData { + private String type; + private Object id; + private String name; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java new file mode 100644 index 00000000..5806fd02 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/DummyClass.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.core.object; + +/** + * 哑元对象,主要用于注解中的缺省对象占位符。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DummyClass { + + private static final Object EMPTY_OBJECT = new Object(); + + /** + * 可以忽略的空对象。避免sonarqube的各种警告。 + * + * @return 空对象。 + */ + public static Object emptyObject() { + return EMPTY_OBJECT; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DummyClass() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java new file mode 100644 index 00000000..01b0d437 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/GlobalThreadLocal.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.util.BooleanUtil; + +/** + * 线程本地化数据管理的工具类。可根据需求自行添加更多的线程本地化变量及其操作方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class GlobalThreadLocal { + + /** + * 存储数据权限过滤是否启用的线程本地化对象。 + * 目前的过滤条件,包括数据权限和租户过滤。 + */ + private static final ThreadLocal DATA_FILTER_ENABLE = ThreadLocal.withInitial(() -> Boolean.TRUE); + + /** + * 设置数据过滤是否打开。如果打开,当前Servlet线程所执行的SQL操作,均会进行数据过滤。 + * + * @param enable 打开为true,否则false。 + * @return 返回之前的状态,便于恢复。 + */ + public static boolean setDataFilter(boolean enable) { + boolean oldValue = DATA_FILTER_ENABLE.get(); + DATA_FILTER_ENABLE.set(enable); + return oldValue; + } + + /** + * 判断当前Servlet线程所执行的SQL操作,是否进行数据过滤。 + * + * @return true 进行数据权限过滤,否则false。 + */ + public static boolean enabledDataFilter() { + return BooleanUtil.isTrue(DATA_FILTER_ENABLE.get()); + } + + /** + * 清空该存储数据,主动释放线程本地化存储资源。 + */ + public static void clearDataFilter() { + DATA_FILTER_ENABLE.remove(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalThreadLocal() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java new file mode 100644 index 00000000..d33a5908 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/LoginUserInfo.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; + +/** + * 在线登录用户信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString +@Slf4j +public class LoginUserInfo { + + /** + * 用户Id。 + */ + private Long userId; + /** + * 用户所在部门Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long deptId; + /** + * 租户Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long tenantId; + /** + * 是否为超级管理员。 + */ + private Boolean isAdmin; + /** + * 用户登录名。 + */ + private String loginName; + /** + * 用户显示名称。 + */ + private String showName; + /** + * 标识不同登录的会话Id。 + */ + private String sessionId; + /** + * 登录IP。 + */ + private String loginIp; + /** + * 登录时间。 + */ + private Date loginTime; + /** + * 登录设备类型。 + */ + private String deviceType; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java new file mode 100644 index 00000000..02131aa6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupCriteria.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.core.object; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Mybatis Mapper.xml中所需的分组条件对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@AllArgsConstructor +public class MyGroupCriteria { + + /** + * GROUP BY 从句后面的参数。 + */ + private String groupBy; + /** + * SELECT 从句后面的分组显示字段。 + */ + private String groupSelect; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java new file mode 100644 index 00000000..81fc69b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyGroupParam.java @@ -0,0 +1,231 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.config.CoreProperties; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.InvalidClassFieldException; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.InvalidDataModelException; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * 查询分组参数请求对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Data +public class MyGroupParam extends ArrayList { + + private final transient CoreProperties coreProperties = + ApplicationContextHolder.getBean(CoreProperties.class); + + /** + * SQL语句的SELECT LIST中,分组字段的返回字段名称列表。 + */ + private List selectGroupFieldList; + /** + * 分组参数解析后构建的SQL语句中所需的分组数据,如GROUP BY的字段列表和SELECT LIST中的分组字段显示列表。 + */ + private transient MyGroupCriteria groupCriteria; + /** + * 基于分组参数对象中的数据,构建SQL中select list和group by从句可以直接使用的分组对象。 + * + * @param groupParam 分组参数对象。 + * @param modelClazz 查询表对应的主对象的Class。 + * @return SQL中所需的GROUP对象。详见MyGroupCriteria类定义。 + */ + public static MyGroupParam buildGroupBy(MyGroupParam groupParam, Class modelClazz) { + if (groupParam == null) { + return null; + } + if (modelClazz == null) { + throw new IllegalArgumentException("modelClazz Argument can't be NULL"); + } + groupParam.selectGroupFieldList = new LinkedList<>(); + StringBuilder groupByBuilder = new StringBuilder(128); + StringBuilder groupSelectBuilder = new StringBuilder(128); + int i = 0; + for (GroupInfo groupInfo : groupParam) { + GroupBaseData groupBaseData = groupParam.parseGroupBaseData(groupInfo, modelClazz); + if (StrUtil.isBlank(groupBaseData.tableName)) { + throw new InvalidDataModelException(groupBaseData.modelName); + } + if (StrUtil.isBlank(groupBaseData.columnName)) { + throw new InvalidDataFieldException(groupBaseData.modelName, groupBaseData.fieldName); + } + groupParam.processGroupInfo(groupInfo, groupBaseData, groupByBuilder, groupSelectBuilder); + String aliasName = StrUtil.isBlank(groupInfo.aliasName) ? groupInfo.fieldName : groupInfo.aliasName; + // selectGroupFieldList中的元素,目前只是被export操作使用。会根据集合中的元素名称匹配导出表头。 + groupParam.selectGroupFieldList.add(aliasName); + if (++i < groupParam.size()) { + groupByBuilder.append(", "); + groupSelectBuilder.append(", "); + } + } + groupParam.groupCriteria = new MyGroupCriteria(groupByBuilder.toString(), groupSelectBuilder.toString()); + return groupParam; + } + + private GroupBaseData parseGroupBaseData(GroupInfo groupInfo, Class modelClazz) { + GroupBaseData baseData = new GroupBaseData(); + if (StrUtil.isBlank(groupInfo.fieldName)) { + throw new IllegalArgumentException("GroupInfo.fieldName can't be EMPTY"); + } + String[] stringArray = StrUtil.splitToArray(groupInfo.fieldName, '.'); + if (stringArray.length == 1) { + baseData.modelName = modelClazz.getSimpleName(); + baseData.fieldName = groupInfo.fieldName; + baseData.tableName = MyModelUtil.mapToTableName(modelClazz); + baseData.columnName = MyModelUtil.mapToColumnName(groupInfo.fieldName, modelClazz); + } else { + Field field = ReflectUtil.getField(modelClazz, stringArray[0]); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]); + } + Class fieldClazz = field.getType(); + baseData.modelName = fieldClazz.getSimpleName(); + baseData.fieldName = stringArray[1]; + baseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + baseData.columnName = MyModelUtil.mapToColumnName(baseData.fieldName, fieldClazz); + } + return baseData; + } + + private void processGroupInfo( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String tableName = baseData.tableName; + String columnName = baseData.columnName; + if (StrUtil.isBlank(groupInfo.dateAggregateBy)) { + groupBy.append(tableName).append(".").append(columnName); + groupSelect.append(tableName).append(".").append(columnName); + if (StrUtil.isNotBlank(groupInfo.aliasName)) { + groupSelect.append(" ").append(groupInfo.aliasName); + } + return; + } + if (coreProperties.isMySql() || coreProperties.isDm()) { + this.processMySqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else if (coreProperties.isPostgresql() || coreProperties.isOpenGauss()) { + this.processPostgreSqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else if (coreProperties.isOracle() || coreProperties.isKingbase()) { + this.processOracleGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else { + throw new UnsupportedOperationException("Unsupport Database Type."); + } + if (StrUtil.isNotBlank(groupInfo.aliasName)) { + groupSelect.append(" ").append(groupInfo.aliasName); + } else { + groupSelect.append(" ").append(columnName); + } + } + + private void processMySqlGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + groupBy.append("DATE_FORMAT(") + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append("DATE_FORMAT(") + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-%m-%d')"); + groupSelect.append(", '%Y-%m-%d')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-%m-01')"); + groupSelect.append(", '%Y-%m-01')"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-01-01')"); + groupSelect.append(", '%Y-01-01')"); + } else { + throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list."); + } + } + + private void processPostgreSqlGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String toCharFunc = "TO_CHAR("; + String dateFormat = ", 'YYYY-MM-dd')"; + groupBy.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(dateFormat); + groupSelect.append(dateFormat); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-01-01')"); + groupSelect.append(", 'YYYY-01-01')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-MM-01')"); + groupSelect.append(", 'YYYY-MM-01')"); + } else { + throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list."); + } + } + + private void processOracleGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String toCharFunc = "TO_CHAR("; + String dateFormat = ", 'YYYY-MM-dd')"; + groupBy.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(dateFormat); + groupSelect.append(dateFormat); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-MM') || '-01'"); + groupSelect.append(", 'YYYY-MM') || '-01'"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY') || '-01-01'"); + groupSelect.append(", 'YYYY') || '-01-01'"); + } else { + throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list."); + } + } + + /** + * 分组信息对象。 + */ + @Data + public static class GroupInfo { + /** + * Java对象的字段名。目前主要包含三种格式: + * 1. 简单的属性名称,如userId,将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。 + * 映射结果或为 my_main_table.user_id + * 2. 一对一关联表属性,如user.userId,这里将先获取user属性的对象类型并映射到对应的表名,后面的userId为 + * user所在实体的属性。映射结果或为:my_sys_user.user_id + */ + private String fieldName; + /** + * SQL语句的Select List中,分组字段的别名。如果别名为NULL,直接取fieldName。 + */ + private String aliasName; + /** + * 如果该值不为NULL,则会对分组字段进行DATE_FORMAT函数的计算,并根据具体的值,将日期数据截取到指定的位。 + * day: 表示按照天聚合,将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d') + * month: 表示按照月聚合,将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01') + * year: 表示按照年聚合,将会截取到年。DATE_FORMAT(columnName, '%Y-01-01') + */ + private String dateAggregateBy; + } + + private static class GroupBaseData { + private String modelName; + private String fieldName; + private String tableName; + private String columnName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java new file mode 100644 index 00000000..9d2ca7b2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyOrderParam.java @@ -0,0 +1,303 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.util.ReflectUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.InvalidClassFieldException; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.InvalidDataModelException; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Controller参数中的排序请求对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Data +public class MyOrderParam extends ArrayList { + + private static final String DICT_MAP = "DictMap."; + private static final Map, MyOrderParam> DEFAULT_ORDER_PARAM_MAP = new ConcurrentHashMap<>(); + + /** + * 基于排序对象中的JSON数据,构建SQL中order by从句可以直接使用的排序字符串。 + * 注意:如果orderParam为NULL,则会通过modelClazz对象推演出主键字典名,并按照主键倒排的方式生成默认的排序对象。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @return SQL中order by从句可以直接使用的排序字符串。 + */ + public static String buildOrderBy(MyOrderParam orderParam, Class modelClazz) { + return buildOrderBy(orderParam, modelClazz, true); + } + + /** + * 基于排序对象中的JSON数据,构建SQL中order by从句可以直接使用的排序字符串。 + * 注意:如果orderParam为NULL,则会通过modelClazz对象推演出主键字典名,并按照主键倒排的方式生成默认的排序对象。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param addDefaultIfNull 如果为true,当orderParam参数为NULL是,则自动添加基于主键倒排序的索引。 + * @return SQL中order by从句可以直接使用的排序字符串。 + */ + public static String buildOrderBy(MyOrderParam orderParam, Class modelClazz, boolean addDefaultIfNull) { + if (orderParam == null) { + if (!addDefaultIfNull) { + return null; + } + orderParam = getAndSetDefaultOrderParam(modelClazz); + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.buildOrderBy can't be NULL"); + } + int i = 0; + StringBuilder orderBy = new StringBuilder(128); + for (OrderInfo orderInfo : orderParam) { + if (StringUtils.isBlank(orderInfo.getFieldName())) { + continue; + } + OrderBaseData orderBaseData = parseOrderBaseData(orderInfo, modelClazz); + if (StringUtils.isBlank(orderBaseData.tableName)) { + throw new InvalidDataModelException(orderBaseData.modelName); + } + if (StringUtils.isBlank(orderBaseData.columnName)) { + throw new InvalidDataFieldException(orderBaseData.modelName, orderBaseData.fieldName); + } + processOrderInfo(orderInfo, orderBaseData, orderBy); + if (++i < orderParam.size()) { + orderBy.append(", "); + } + } + return orderBy.toString(); + } + + private static MyOrderParam getAndSetDefaultOrderParam(Class modelClazz) { + MyOrderParam orderParam = DEFAULT_ORDER_PARAM_MAP.get(modelClazz); + if (orderParam != null) { + return orderParam; + } + orderParam = new MyOrderParam(); + DEFAULT_ORDER_PARAM_MAP.put(modelClazz, orderParam); + Field[] fields = ReflectUtil.getFields(modelClazz); + for (Field field : fields) { + if (field.getAnnotation(TableId.class) != null) { + orderParam.add(new OrderInfo(field.getName(), false, null)); + break; + } + } + return orderParam; + } + + private static void processOrderInfo( + OrderInfo orderInfo, OrderBaseData orderBaseData, StringBuilder orderByBuilder) { + if (StringUtils.isNotBlank(orderInfo.dateAggregateBy)) { + orderByBuilder.append("DATE_FORMAT(") + .append(orderBaseData.tableName).append(".").append(orderBaseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-%m-%d')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-%m-01')"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-01-01')"); + } else { + throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list."); + } + } else { + orderByBuilder.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName); + } + if (orderInfo.asc != null && !orderInfo.asc) { + orderByBuilder.append(" DESC"); + } + } + + private static OrderBaseData parseOrderBaseData(OrderInfo orderInfo, Class modelClazz) { + OrderBaseData orderBaseData = new OrderBaseData(); + orderBaseData.fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + String[] stringArray = StringUtils.split(orderBaseData.fieldName, '.'); + if (stringArray.length == 1) { + orderBaseData.modelName = modelClazz.getSimpleName(); + orderBaseData.tableName = MyModelUtil.mapToTableName(modelClazz); + orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, modelClazz); + } else { + Field field = ReflectUtil.getField(modelClazz, stringArray[0]); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]); + } + Class fieldClazz = field.getType(); + orderBaseData.modelName = fieldClazz.getSimpleName(); + orderBaseData.fieldName = stringArray[1]; + orderBaseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, fieldClazz); + } + return orderBaseData; + } + + /** + * 在排序列表中,可能存在基于指定表字段的排序,该函数将获取指定表的所有排序字段。 + * 返回的字符串,可直接用于SQL中的ORDER BY从句。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param relationModelName 与关联表对应的Model的名称,如my_course_paper表应对的Java对象CoursePaper。 + * 如果该值为null或空字符串,则获取所有主表的排序字段。 + * @return 返回的是表字段,而非Java对象的属性,多个字段之间逗号分隔。 + */ + public static String getOrderClauseByModelName( + MyOrderParam orderParam, Class modelClazz, String relationModelName) { + if (orderParam == null) { + return null; + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.getOrderClauseByModelName can't be NULL"); + } + List fieldNameList = new LinkedList<>(); + String prefix = null; + if (StringUtils.isNotBlank(relationModelName)) { + prefix = relationModelName + "."; + } + for (OrderInfo orderInfo : orderParam) { + OrderBaseData baseData = parseOrderBaseData(orderInfo, modelClazz, prefix, relationModelName); + if (baseData != null) { + fieldNameList.add(makeOrderBy(baseData, orderInfo.asc)); + } + } + return StringUtils.join(fieldNameList, ", "); + } + + private static OrderBaseData parseOrderBaseData( + OrderInfo orderInfo, Class modelClazz, String prefix, String relationModelName) { + OrderBaseData baseData = null; + String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + if (prefix != null) { + if (fieldName.startsWith(prefix)) { + baseData = new OrderBaseData(); + Field field = ReflectUtil.getField(modelClazz, relationModelName); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), relationModelName); + } + Class fieldClazz = field.getType(); + baseData.modelName = fieldClazz.getSimpleName(); + baseData.fieldName = StringUtils.removeStart(fieldName, prefix); + baseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + baseData.columnName = MyModelUtil.mapToColumnName(fieldName, fieldClazz); + } + } else { + String dotLimitor = "."; + if (!fieldName.contains(dotLimitor)) { + baseData = new OrderBaseData(); + baseData.modelName = modelClazz.getSimpleName(); + baseData.tableName = MyModelUtil.mapToTableName(modelClazz); + baseData.columnName = MyModelUtil.mapToColumnName(fieldName, modelClazz); + } + } + return baseData; + } + + private static String makeOrderBy(OrderBaseData baseData, Boolean asc) { + if (StringUtils.isBlank(baseData.tableName)) { + throw new InvalidDataModelException(baseData.modelName); + } + if (StringUtils.isBlank(baseData.columnName)) { + throw new InvalidDataFieldException(baseData.modelName, baseData.fieldName); + } + StringBuilder orderBy = new StringBuilder(128); + orderBy.append(baseData.tableName).append(".").append(baseData.columnName); + if (asc != null && !asc) { + orderBy.append(" DESC"); + } + return orderBy.toString(); + } + + /** + * 在排序列表中,可能存在基于指定表字段的排序,该函数将删除指定表的所有排序字段。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param relationModelName 与关联表对应的Model的名称,如my_course_paper表应对的Java对象CoursePaper。 + * 如果该值为null或空字符串,则获取所有主表的排序字段。 + */ + public static void removeOrderClauseByModelName( + MyOrderParam orderParam, Class modelClazz, String relationModelName) { + if (orderParam == null) { + return; + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.removeOrderClauseByModelName can't be NULL"); + } + List fieldIndexList = new LinkedList<>(); + String prefix = null; + if (StringUtils.isNotBlank(relationModelName)) { + prefix = relationModelName + "."; + } + int i = 0; + for (OrderInfo orderInfo : orderParam) { + String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + if (prefix != null) { + if (fieldName.startsWith(prefix)) { + fieldIndexList.add(i); + } + } else { + if (!fieldName.contains(".")) { + fieldIndexList.add(i); + } + } + ++i; + } + for (int index : fieldIndexList) { + orderParam.remove(index); + } + } + + /** + * 排序信息对象。 + */ + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class OrderInfo { + /** + * Java对象的字段名。如果fieldName为空,则忽略跳过。目前主要包含三种格式: + * 1. 简单的属性名称,如userId,将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。 + * 映射结果或为 my_main_table.user_id + * 2. 字典属性名称,如userIdDictMap.id,由于仅仅支持字典中Id数据的排序,所以直接截取DictMap之前的字符串userId作为排序属性。 + * 表名为当前ModelClazz所对应的表名。映射结果或为 my_main_table.user_id + * 3. 一对一关联表属性,如user.userId,这里将先获取user属性的对象类型并映射到对应的表名,后面的userId为 + * user所在实体的属性。映射结果或为:my_sys_user.user_id + */ + private String fieldName; + /** + * 排序方向。true为升序,否则降序。 + */ + private Boolean asc = true; + /** + * 如果该值不为NULL,则会对日期型排序字段进行DATE_FORMAT函数的计算,并根据具体的值,将日期数据截取到指定的位。 + * day: 表示按照天聚合,将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d') + * month: 表示按照月聚合,将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01') + * year: 表示按照年聚合,将会截取到年。DATE_FORMAT(columnName, '%Y-01-01') + */ + private String dateAggregateBy; + } + + private static class OrderBaseData { + private String modelName; + private String fieldName; + private String tableName; + private String columnName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java new file mode 100644 index 00000000..57bb1c8f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageData.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.core.object; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.LinkedList; +import java.util.List; + +/** + * 分页数据的应答返回对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MyPageData { + /** + * 数据列表。 + */ + private List dataList; + /** + * 数据总数量。 + */ + private Long totalCount; + + /** + * 为了保持前端的数据格式兼容性,在没有数据的时候,需要返回空分页对象。 + * @return 空分页对象。 + */ + public static MyPageData emptyPageData() { + return new MyPageData<>(new LinkedList<>(), 0L); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java new file mode 100644 index 00000000..cd4ddc41 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPageParam.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.core.object; + +import lombok.Getter; + +/** + * Controller参数中的分页请求对象 + * + * @author Jerry + * @date 2024-07-02 + */ +@Getter +public class MyPageParam { + + public static final int DEFAULT_PAGE_NUM = 1; + public static final int DEFAULT_PAGE_SIZE = 10; + public static final int DEFAULT_MAX_SIZE = 2000; + + /** + * 分页号码,从1开始计数。 + */ + private Integer pageNum; + + /** + * 每页大小。 + */ + private Integer pageSize; + + /** + * 是否统计totalCount + */ + private Boolean count = true; + + /** + * 设置当前分页页号。 + * + * @param pageNum 页号,如果传入非法值,则使用缺省值。 + */ + public void setPageNum(Integer pageNum) { + if (pageNum == null) { + return; + } + if (pageNum <= 0) { + pageNum = DEFAULT_PAGE_NUM; + } + this.pageNum = pageNum; + } + + /** + * 设置分页的大小。 + * + * @param pageSize 分页大小,如果传入非法值,则使用缺省值。 + */ + public void setPageSize(Integer pageSize) { + if (pageSize == null) { + return; + } + if (pageSize <= 0) { + pageSize = DEFAULT_PAGE_SIZE; + } + if (pageSize > DEFAULT_MAX_SIZE) { + pageSize = DEFAULT_MAX_SIZE; + } + this.pageSize = pageSize; + } + + public void setCount(Boolean count) { + this.count = count; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java new file mode 100644 index 00000000..6a5a60d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyPrintInfo.java @@ -0,0 +1,32 @@ +package com.orangeforms.common.core.object; + +import com.alibaba.fastjson.JSONArray; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 打印信息对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +public class MyPrintInfo { + + /** + * 打印模板Id。 + */ + private Long printId; + /** + * 打印参数列表。对应于common-report模块的ReportPrintParam对象。 + */ + private List printParams; + + public MyPrintInfo(Long printId, List printParams) { + this.printId = printId; + this.printParams = printParams; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java new file mode 100644 index 00000000..26f23c15 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyRelationParam.java @@ -0,0 +1,122 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 实体对象数据组装参数构建器。 + * BaseService中的实体对象数据组装函数,会根据该参数对象进行数据组装。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@Builder +public class MyRelationParam { + + /** + * 是否组装字典关联的标记。 + * 组装RelationDict和RelationConstDict注解标记的字段。 + */ + private boolean buildDict; + + /** + * 是否组装一对一关联的标记。 + * 组装RelationOneToOne注解标记的字段。 + */ + private boolean buildOneToOne; + + /** + * 是否组装一对多关联的标记。 + * 组装RelationOneToMany注解标记的字段。 + */ + private boolean buildOneToMany; + + /** + * 在组装一对一关联的同时,是否继续关联从表中的字典。 + * 从表中RelationDict和RelationConstDict注解标记的字段。 + * 该字段为true时,无需设置buildOneToOne了。 + */ + private boolean buildOneToOneWithDict; + + /** + * 是否组装主表对多对多中间表关联的标记。 + * 组装RelationManyToMany注解标记的字段。 + */ + private boolean buildRelationManyToMany; + + /** + * 是否组装聚合计算关联的标记。 + * 组装RelationOneToManyAggregation和RelationManyToManyAggregation注解标记的字段。 + */ + private boolean buildRelationAggregation; + + /** + * 关联表中,需要忽略的脱敏字段名。key是关联表实体对象名,如SysUser,value是对象字段名的集合,如userId。 + */ + @Getter + private Map> ignoreMaskFieldMap; + + /** + * 关联表中需要忽略的脱敏字段结合。 + * @param ignoreRelationMaskFieldSet 数据项格式为"实体对象名.对象属性名",如 sysUser.userId。 + */ + public void setIgnoreMaskFieldSet(Set ignoreRelationMaskFieldSet) { + if (CollUtil.isEmpty(ignoreRelationMaskFieldSet)) { + return; + } + ignoreMaskFieldMap = MapUtil.newHashMap(); + for (String ignoreField : ignoreRelationMaskFieldSet) { + String[] fullFieldName = StrUtil.splitToArray(ignoreField, "."); + Set ignoreMaskFieldSet = + ignoreMaskFieldMap.computeIfAbsent(fullFieldName[0], k -> new HashSet<>()); + ignoreMaskFieldSet.add(fullFieldName[1]); + } + } + + /** + * 便捷方法,返回仅做字典关联的参数对象。 + * + * @return 返回仅做字典关联的参数对象。 + */ + public static MyRelationParam dictOnly() { + return MyRelationParam.builder().buildDict(true).build(); + } + + /** + * 便捷方法,返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。 + * NOTE: 对于一对多和多对多,这种从表数据是列表结果的关联,均不返回。 + * + * @return 返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。 + */ + public static MyRelationParam normal() { + return MyRelationParam.builder() + .buildDict(true) + .buildOneToOneWithDict(true) + .buildRelationAggregation(true) + .build(); + } + + /** + * 便捷方法,返回全部关联的参数对象。 + * + * @return 返回全部关联的参数对象。 + */ + public static MyRelationParam full() { + return MyRelationParam.builder() + .buildDict(true) + .buildOneToOneWithDict(true) + .buildRelationAggregation(true) + .buildRelationManyToMany(true) + .buildOneToMany(true) + .build(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java new file mode 100644 index 00000000..d225446c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/MyWhereCriteria.java @@ -0,0 +1,376 @@ +package com.orangeforms.common.core.object; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.alibaba.fastjson.annotation.JSONField; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.exception.InvalidDataModelException; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; + +/** + * Where中的条件语句。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Data +@NoArgsConstructor +public class MyWhereCriteria { + + /** + * 等于 + */ + public static final int OPERATOR_EQUAL = 0; + + /** + * 不等于 + */ + public static final int OPERATOR_NOT_EQUAL = 1; + + /** + * 大于等于 + */ + public static final int OPERATOR_GE = 2; + + /** + * 大于 + */ + public static final int OPERATOR_GT = 3; + + /** + * 小于等于 + */ + public static final int OPERATOR_LE = 4; + + /** + * 小于 + */ + public static final int OPERATOR_LT = 5; + + /** + * LIKE + */ + public static final int OPERATOR_LIKE = 6; + + /** + * NOT NULL + */ + public static final int OPERATOR_NOT_NULL = 7; + + /** + * IS NULL + */ + public static final int OPERATOR_IS_NULL = 8; + + /** + * IN + */ + public static final int OPERATOR_IN = 9; + + /** + * 参与过滤的实体对象的Class。 + */ + @JSONField(serialize = false) + private Class modelClazz; + + /** + * 数据库表名。 + */ + private String tableName; + + /** + * Java属性名称。 + */ + private String fieldName; + + /** + * 数据表字段名。 + */ + private String columnName; + + /** + * 数据表字段类型。 + */ + private Integer columnType; + + /** + * 操作符类型,取值范围见上面的常量值。 + */ + private Integer operatorType; + + /** + * 条件数据值。 + */ + private Object value; + + public MyWhereCriteria(Class modelClazz, String fieldName, Integer operatorType, Object value) { + this.modelClazz = modelClazz; + this.fieldName = fieldName; + this.operatorType = operatorType; + this.value = value; + } + + /** + * 设置条件值。 + * + * @param fieldName 条件所属的实体对象的字段名。 + * @param operatorType 条件操作符。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult setCriteria(String fieldName, Integer operatorType, Object value) { + this.operatorType = operatorType; + this.fieldName = fieldName; + this.value = value; + return doVerify(); + } + + /** + * 设置条件值。 + * + * @param modelClazz 数据表对应实体对象的Class. + * @param fieldName 条件所属的实体对象的字段名。 + * @param operatorType 条件操作符。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult setCriteria(Class modelClazz, String fieldName, Integer operatorType, Object value) { + this.modelClazz = modelClazz; + this.operatorType = operatorType; + this.fieldName = fieldName; + this.value = value; + return doVerify(); + } + + /** + * 设置条件值,通过该构造方法设置时,通常是直接将表名、字段名、字段类型等赋值,无需在通过modelClazz进行推演。 + * + * @param tableName 数据表名。 + * @param columnName 数据字段名。 + * @param columnType 数据字段类型。 + * @param operatorType 操作类型。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + */ + public void setCriteria( + String tableName, String columnName, String columnType, Integer operatorType, Object value) { + this.tableName = tableName; + this.columnName = columnName; + this.columnType = MyModelUtil.NUMERIC_FIELD_TYPE; + if (String.class.getSimpleName().equals(columnType)) { + this.columnType = MyModelUtil.STRING_FIELD_TYPE; + } else if (Date.class.getSimpleName().equals(columnType)) { + this.columnType = MyModelUtil.DATE_FIELD_TYPE; + } + this.operatorType = operatorType; + this.value = value; + } + + /** + * 在执行该函数之前,该对象的所有数据均已经赋值完毕。 + * 该函数主要验证操作符字段和条件值字段对应关系的合法性。 + * + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult doVerify() { + if (fieldName == null) { + return CallResult.error("过滤字段名称 [fieldName] 不能为空!"); + } + if (modelClazz != null && ReflectUtil.getField(modelClazz, fieldName) == null) { + return CallResult.error( + "过滤字段 [" + fieldName + "] 在实体对象 [" + modelClazz.getSimpleName() + "] 中并不存在!"); + } + if (!checkOperatorType()) { + return CallResult.error("无效的操作符类型 [" + operatorType + "]!"); + } + // 其他操作符必须包含value值 + if (operatorType != OPERATOR_IS_NULL && operatorType != OPERATOR_NOT_NULL && value == null) { + String operatorString = this.getOperatorString(); + return CallResult.error("操作符 [" + operatorString + "] 的条件值不能为空!"); + } + if (this.operatorType == OPERATOR_IN) { + if (!(value instanceof Collection)) { + return CallResult.error("操作符 [IN] 的条件值必须为集合对象!"); + } + if (CollUtil.isEmpty((Collection) value)) { + return CallResult.error("操作符 [IN] 的条件值不能为空!"); + } + } + return CallResult.ok(); + } + + /** + * 判断操作符类型是否合法。 + * + * @return 合法返回true,否则false。 + */ + public boolean checkOperatorType() { + return operatorType != null + && (operatorType >= OPERATOR_EQUAL && operatorType <= OPERATOR_IN); + } + + /** + * 获取操作符的字符串形式。 + * + * @return 操作符的字符串。 + */ + public String getOperatorString() { + switch (operatorType) { + case OPERATOR_EQUAL: + return " = "; + case OPERATOR_NOT_EQUAL: + return " != "; + case OPERATOR_GE: + return " >= "; + case OPERATOR_GT: + return " > "; + case OPERATOR_LE: + return " <= "; + case OPERATOR_LT: + return " < "; + case OPERATOR_LIKE: + return " LIKE "; + case OPERATOR_NOT_NULL: + return " IS NOT NULL "; + case OPERATOR_IS_NULL: + return " IS NULL "; + case OPERATOR_IN: + return " IN "; + default: + return null; + } + } + + /** + * 获取组装后的SQL Where从句,如 table_name.column_name = 'value'。 + * 与查询数据表对应的实体对象Class为当前对象的modelClazz字段。 + * + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public String makeCriteriaString() { + return makeCriteriaString(this.modelClazz); + } + + /** + * 获取组装后的SQL Where从句,如 table_name.column_name = 'value'。 + * + * @param modelClazz 与查询数据表对应的实体对象的Class。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @exception InvalidDataModelException 参数modelClazz没有对应的table,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public String makeCriteriaString(Class modelClazz) { + String localTableName; + String localColumnName; + Integer localColumnType; + if (modelClazz != null) { + Tuple2 fieldInfo = MyModelUtil.mapToColumnInfo(fieldName, modelClazz); + if (fieldInfo == null) { + throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName); + } + localColumnName = fieldInfo.getFirst(); + localColumnType = fieldInfo.getSecond(); + localTableName = MyModelUtil.mapToTableName(modelClazz); + if (localTableName == null) { + throw new InvalidDataModelException(modelClazz.getSimpleName()); + } + } else { + localTableName = this.tableName; + localColumnName = this.columnName; + localColumnType = this.columnType; + } + return this.buildClauseString(localTableName, localColumnName, localColumnType); + } + + /** + * 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。 + * + * @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public static String makeCriteriaString(List criteriaList) { + return makeCriteriaString(criteriaList, null); + } + + /** + * 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。 + * + * @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。 + * @param modelClazz 与数据表对应的实体对象的Class。 + * 如果不为NULL实体对象Class使用该值,否则使用每个MyWhereCriteria自身的modelClazz。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public static String makeCriteriaString(List criteriaList, Class modelClazz) { + if (CollUtil.isEmpty(criteriaList)) { + return null; + } + StringBuilder sb = new StringBuilder(256); + int i = 0; + for (MyWhereCriteria whereCriteria : criteriaList) { + Class clazz = modelClazz; + if (clazz == null) { + clazz = whereCriteria.modelClazz; + } + if (i++ != 0) { + sb.append(" AND "); + } + String criteriaString = whereCriteria.makeCriteriaString(clazz); + sb.append(criteriaString); + } + return sb.length() == 0 ? null : sb.toString(); + } + + private String buildClauseString(String tableName, String columnName, Integer columnType) { + StringBuilder sb = new StringBuilder(64); + sb.append(tableName).append(".").append(columnName).append(getOperatorString()); + if (operatorType == OPERATOR_IN) { + Collection filterValues = (Collection) value; + sb.append("("); + int i = 0; + for (Object filterValue : filterValues) { + this.doSqlInjectVerify(filterValue.toString()); + if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) { + sb.append(filterValue); + } else { + sb.append("'").append(filterValue).append("'"); + } + if (i++ != filterValues.size() - 1) { + sb.append(", "); + } + } + sb.append(")"); + return sb.toString(); + } + if (value == null) { + return sb.toString(); + } + this.doSqlInjectVerify(value.toString()); + if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) { + sb.append(value); + } else { + sb.append("'").append(value).append("'"); + } + return sb.toString(); + } + + private void doSqlInjectVerify(String v) { + Matcher matcher = ApplicationConstant.SQL_INJECT_PATTERN.matcher(v); + if (matcher.find()) { + String msg = String.format( + "The filterValue [%s] has SQL Inject Words", v); + throw new MyRuntimeException(msg); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java new file mode 100644 index 00000000..26e2eee5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/ResponseResult.java @@ -0,0 +1,295 @@ +package com.orangeforms.common.core.object; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * 接口返回对象 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Data +public class ResponseResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final ResponseResult OK = new ResponseResult<>(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误码。 + */ + private String errorCode = "NO-ERROR"; + /** + * 错误信息描述。 + */ + private String errorMessage = "NO-MESSAGE"; + /** + * 实际数据。 + */ + private T data = null; + /** + * HTTP状态码,通常用于内部调用的方法传递,不推荐返回给前端。 + */ + @JSONField(serialize = false) + private int httpStatus = 200; + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param errorCodeEnum 错误码枚举。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum) { + return create(errorCodeEnum, errorCodeEnum.getErrorMessage()); + } + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 如果该参数为null,错误信息取自errorCodeEnum参数内置的errorMessage,否则使用当前参数。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum, String errorMessage) { + errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage(); + return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success() : error(errorCodeEnum.name(), errorMessage); + } + + /** + * 根据参数errorCode是否为空,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。 + * + * @param errorCode 自定义的错误码。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(String errorCode, String errorMessage) { + return errorCode == null ? success() : error(errorCode, errorMessage); + } + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 如果该参数为null,错误信息取自errorCodeEnum参数内置的errorMessage,否则使用当前参数。 + * @param data 如果错误枚举值为NO_ERROR,则返回该数据。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum, String errorMessage, T data) { + errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage(); + return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success(data) : error(errorCodeEnum.name(), errorMessage); + } + + /** + * 创建成功对象。 + * 如果需要绑定返回数据,可以在实例化后调用setDataObject方法。 + * + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult success() { + return OK; + } + + /** + * 创建带有返回数据的成功对象。 + * + * @param data 返回的数据对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult success(T data) { + ResponseResult resp = new ResponseResult<>(); + resp.data = data; + return resp; + } + + /** + * 创建带有返回数据的成功对象。 + * + * @param data 返回的数据对象。 + * @param clazz 目标数据类型。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult success(R data, Class clazz) { + ResponseResult resp = new ResponseResult<>(); + resp.data = MyModelUtil.copyTo(data, clazz); + return resp; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param errorCodeEnum 错误码枚举。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(ErrorCodeEnum errorCodeEnum) { + return error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage()); + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param httpStatus http状态值。 + * @param errorCodeEnum 错误码枚举。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(int httpStatus, ErrorCodeEnum errorCodeEnum) { + ResponseResult r = error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage()); + r.setHttpStatus(httpStatus); + return r; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(ErrorCodeEnum errorCodeEnum, String errorMessage) { + return error(errorCodeEnum.name(), errorMessage); + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param httpStatus http状态值。 + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(int httpStatus, ErrorCodeEnum errorCodeEnum, String errorMessage) { + ResponseResult r = error(errorCodeEnum.name(), errorMessage); + r.setHttpStatus(httpStatus); + return r; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。 + * + * @param errorCode 自定义的错误码。 + * @param errorMessage 自定义的错误信息。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult error(String errorCode, String errorMessage) { + return new ResponseResult<>(errorCode, errorMessage); + } + + /** + * 根据参数中出错的ResponseResult,创建新的错误应答对象。 + * + * @param errorCause 导致错误原因的应答对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult errorFrom(ResponseResult errorCause) { + return error(errorCause.errorCode, errorCause.getErrorMessage()); + } + + /** + * 根据参数中出错的CallResult,创建新的错误应答对象。 + * + * @param errorCause 导致错误原因的应答对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult errorFrom(CallResult errorCause) { + return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorCause.getErrorMessage()); + } + + /** + * 根据参数中CallResult,创建新的应答对象。 + * + * @param result CallResult对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult from(CallResult result) { + if (result.isSuccess()) { + return success(); + } + return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + + /** + * 是否成功。 + * + * @return true成功,否则false。 + */ + public boolean isSuccess() { + return success; + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。 + * + * @param httpStatus http状态码。 + * @param responseResult 应答内容。 + * @param 数据对象类型。 + * @throws IOException 异常错误。 + */ + public static void output(int httpStatus, ResponseResult responseResult) throws IOException { + if (httpStatus != HttpServletResponse.SC_OK) { + log.error(JSON.toJSONString(responseResult)); + } else { + log.info(JSON.toJSONString(responseResult)); + } + HttpServletResponse response = ContextUtil.getHttpResponse(); + PrintWriter out = response.getWriter(); + response.setContentType("application/json; charset=utf-8"); + response.setStatus(httpStatus); + if (responseResult != null) { + out.print(JSON.toJSONString(responseResult)); + } + out.flush(); + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。 + * + * @param httpStatus http状态码。 + * @throws IOException 异常错误。 + */ + public static void output(int httpStatus) throws IOException { + output(httpStatus, null); + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。Http状态码为200。 + * + * @param responseResult 应答内容。 + * @param 数据对象类型。 + * @throws IOException 异常错误。 + */ + public static void output(ResponseResult responseResult) throws IOException { + output(HttpServletResponse.SC_OK, responseResult); + } + + private ResponseResult() { + } + + private ResponseResult(String errorCode, String errorMessage) { + this.success = false; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java new file mode 100644 index 00000000..71c9d594 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TableModelInfo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +/** + * 数据表模型基础信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class TableModelInfo { + + /** + * 数据表名。 + */ + private String tableName; + + /** + * 实体对象名。 + */ + private String modelName; + + /** + * 主键的表字段名。 + */ + private String keyColumnName; + + /** + * 主键在实体对象中的属性名。 + */ + private String keyFieldName; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java new file mode 100644 index 00000000..79f3c1f9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TokenData.java @@ -0,0 +1,134 @@ +package com.orangeforms.common.core.object; + +import com.orangeforms.common.core.util.ContextUtil; +import lombok.Data; +import lombok.ToString; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; + +/** + * 基于Jwt,用于前后端传递的令牌对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ToString +public class TokenData { + + /** + * 在HTTP Request对象中的属性键。 + */ + public static final String REQUEST_ATTRIBUTE_NAME = "tokenData"; + /** + * 是否为百分号编码后的TokenData数据。 + */ + public static final String REQUEST_ENCODED_TOKEN = "encodedTokenData"; + /** + * 用户Id。 + */ + private Long userId; + /** + * 用户所属角色。多个角色之间逗号分隔。 + */ + private String roleIds; + /** + * 用户所在部门Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long deptId; + /** + * 用户所属岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。 + */ + private String postIds; + /** + * 用户的部门岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。 + */ + private String deptPostIds; + /** + * 租户Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long tenantId; + /** + * 是否为超级管理员。 + */ + private Boolean isAdmin; + /** + * 用户登录名。 + */ + private String loginName; + /** + * 用户显示名称。 + */ + private String showName; + /** + * 所在部门名。 + */ + private String deptName; + /** + * 设备类型。参考AppDeviceType。 + */ + private String deviceType; + /** + * 标识不同登录的会话Id。 + */ + private String sessionId; + /** + * 目前仅用于SaToken权限框架。 + * 主要用于辅助管理在线用户数据,SaToken默认的功能对于租户Id和登录用户的查询,没有提供方便的支持,或是效率较低。 + */ + private String mySessionId; + /** + * 访问uaa的授权token。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private String uaaAccessToken; + /** + * 数据库路由键(仅当水平分库时使用)。 + */ + private Integer datasourceType; + /** + * 登录IP。 + */ + private String loginIp; + /** + * 登录时间。 + */ + private Date loginTime; + /** + * 登录头像地址。 + */ + private String headImageUrl; + /** + * 原始的请求Token。 + */ + private String token; + /** + * 应用编码。空值表示非第三方应用。 + */ + private String appCode; + + /** + * 将令牌对象添加到Http请求对象。 + * + * @param tokenData 令牌对象。 + */ + public static void addToRequest(TokenData tokenData) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request != null) { + request.setAttribute(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData); + } + } + + /** + * 从Http Request对象中获取令牌对象。 + * + * @return 令牌对象。 + */ + public static TokenData takeFromRequest() { + HttpServletRequest request = ContextUtil.getHttpRequest(); + return request == null ? null : (TokenData) request.getAttribute(REQUEST_ATTRIBUTE_NAME); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java new file mode 100644 index 00000000..19799a3e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple2.java @@ -0,0 +1,50 @@ +package com.orangeforms.common.core.object; + +/** + * 二元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class Tuple2 { + + /** + * 第一个变量。 + */ + private final T1 first; + /** + * 第二个变量。 + */ + private final T2 second; + + /** + * 构造函数。 + * + * @param first 第一个变量。 + * @param second 第二个变量。 + */ + public Tuple2(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + /** + * 获取第一个变量。 + * + * @return 返回第一个变量。 + */ + public T1 getFirst() { + return first; + } + + /** + * 获取第二个变量。 + * + * @return 返回第二个变量。 + */ + public T2 getSecond() { + return second; + } + +} + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java new file mode 100644 index 00000000..bc6e4b7e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/Tuple3.java @@ -0,0 +1,65 @@ +package com.orangeforms.common.core.object; + +/** + * 三元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class Tuple3 { + + /** + * 第一个变量。 + */ + private final T1 first; + /** + * 第二个变量。 + */ + private final T2 second; + + /** + * 第三个变量。 + */ + private final T3 third; + + /** + * 构造函数。 + * + * @param first 第一个变量。 + * @param second 第二个变量。 + * @param third 第三个变量。 + */ + public Tuple3(T1 first, T2 second, T3 third) { + this.first = first; + this.second = second; + this.third = third; + } + + /** + * 获取第一个变量。 + * + * @return 返回第一个变量。 + */ + public T1 getFirst() { + return first; + } + + /** + * 获取第二个变量。 + * + * @return 返回第二个变量。 + */ + public T2 getSecond() { + return second; + } + + /** + * 获取第三个变量。 + * + * @return 返回第三个变量。 + */ + public T3 getThird() { + return third; + } +} + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java new file mode 100644 index 00000000..2dea0ca3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/object/TypedCallResult.java @@ -0,0 +1,109 @@ +package com.orangeforms.common.core.object; + +import lombok.Data; + +/** + * 业务方法调用结果对象。可以同时返回具体的错误和自定义类型的数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class TypedCallResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final TypedCallResult OK = new TypedCallResult<>(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误信息描述。 + */ + private String errorMessage = null; + /** + * 在验证同时,仍然需要附加的关联数据对象。 + */ + private T data; + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static TypedCallResult create(String errorMessage) { + return errorMessage == null ? ok() : error(errorMessage); + } + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @param data 附带的数据对象。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static TypedCallResult create(String errorMessage, T data) { + return errorMessage == null ? ok(data) : error(errorMessage, data); + } + + /** + * 创建表示验证成功的对象实例。 + * + * @return 验证成功对象实例。 + */ + public static TypedCallResult ok() { + return OK; + } + + /** + * 创建表示验证成功的对象实例。 + * + * @param data 附带的数据对象。 + * @return 验证成功对象实例。 + */ + public static TypedCallResult ok(T data) { + TypedCallResult result = new TypedCallResult<>(); + result.data = data; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @return 验证失败对象实例。 + */ + public static TypedCallResult error(String errorMessage) { + TypedCallResult result = new TypedCallResult<>(); + result.success = false; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @param data 附带的数据对象。 + * @return 验证失败对象实例。 + */ + public static TypedCallResult error(String errorMessage, T data) { + TypedCallResult result = new TypedCallResult<>(); + result.success = false; + result.errorMessage = errorMessage; + result.data = data; + return result; + } + + /** + * 根据参数中出错的TypedCallResult,创建新的错误调用结果对象。 + * @param result 错误调用结果对象。 + * @return 新的错误调用结果对象。 + */ + public static TypedCallResult errorFrom(TypedCallResult result) { + return error(result.getErrorMessage()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java new file mode 100644 index 00000000..840610bf --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/BaseUpDownloader.java @@ -0,0 +1,216 @@ +package com.orangeforms.common.core.upload; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * 上传或下载文件抽象父类。 + * 包含存储本地文件的功能,以及上传和下载所需的通用方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class BaseUpDownloader { + + /** + * 构建上传文件的完整目录。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 上传文件的完整路径名。 + */ + public String makeFullPath( + String rootBaseDir, String modelName, String fieldName, Boolean asImage) { + StringBuilder uploadPathBuilder = new StringBuilder(128); + if (StringUtils.isNotBlank(rootBaseDir)) { + uploadPathBuilder.append(rootBaseDir).append("/"); + } + if (Boolean.TRUE.equals(asImage)) { + uploadPathBuilder.append(ApplicationConstant.UPLOAD_IMAGE_PARENT_PATH); + } else { + uploadPathBuilder.append(ApplicationConstant.UPLOAD_ATTACHMENT_PARENT_PATH); + } + if (StringUtils.isNotBlank(modelName)) { + uploadPathBuilder.append("/").append(modelName); + } + if (StringUtils.isNotBlank(fieldName)) { + uploadPathBuilder.append("/").append(fieldName); + } + return uploadPathBuilder.toString(); + } + + /** + * 构建上传文件的完整目录。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param path 文件目录。 + * @return 上传文件的完整路径名。 + */ + public String makeFullPath(String rootBaseDir, String path) { + StringBuilder uploadPathBuilder = new StringBuilder(128); + if (StringUtils.isNotBlank(rootBaseDir)) { + uploadPathBuilder.append(rootBaseDir).append("/"); + } + if (StringUtils.isNotBlank(path)) { + if (!StrUtil.startWith(path, "/")) { + uploadPathBuilder.append("/"); + } + uploadPathBuilder.append(path); + } + return uploadPathBuilder.toString(); + } + + /** + * 构建上传操作的返回对象。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param originalFilename 上传文件的原始文件名(包含扩展名)。 + */ + protected void fillUploadResponseInfo( + UploadResponseInfo responseInfo, String serviceContextPath, String originalFilename) { + // 根据请求上传的uri构建下载uri,只是将末尾的/upload改为/download即可。 + HttpServletRequest request = ContextUtil.getHttpRequest(); + String uri = request.getRequestURI(); + uri = StringUtils.removeEnd(uri, "/"); + uri = StringUtils.removeEnd(uri, "/upload"); + String downloadUri; + if (StringUtils.isBlank(serviceContextPath)) { + downloadUri = uri + "/download"; + } else { + downloadUri = serviceContextPath + uri + "/download"; + } + StringBuilder filenameBuilder = new StringBuilder(64); + filenameBuilder.append(MyCommonUtil.generateUuid()) + .append(".").append(FilenameUtils.getExtension(originalFilename)); + responseInfo.setDownloadUri(downloadUri); + responseInfo.setFilename(filenameBuilder.toString()); + } + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param fileName 文件名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @param response Http 应答对象。 + * @throws IOException 操作错误。 + */ + public abstract void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) throws IOException; + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param uriPath uri中的路径名。 + * @param fileName 文件名。 + * @param response Http 应答对象。 + * @throws IOException 操作错误。 + */ + public abstract void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException; + + /** + * 执行文件上传操作,并存入本地文件系统,再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象,返回给前端。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param rootBaseDir 存放上传文件的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param uploadFile Http请求中上传的文件对象。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 存储在本地上传文件名。 + * @throws IOException 操作错误。 + */ + public abstract UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException; + + /** + * 执行文件上传操作,并存入本地文件系统,再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象,返回给前端。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param rootBaseDir 存放上传文件的根目录。 + * @param uriPath uri中的路径名。 + * @param uploadFile Http请求中上传的文件对象。 + * @return 存储在本地上传文件名。 + * @throws IOException 操作错误。 + */ + public abstract UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String uriPath, + MultipartFile uploadFile) throws IOException; + + /** + * 判断filename参数指定的文件名,是否被包含在fileInfoJson参数中。 + * + * @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。 + * @param filename 被包含的文件名。 + * @return 存在返回true,否则false。 + */ + public static boolean containFile(String fileInfoJson, String filename) { + if (StringUtils.isAnyBlank(fileInfoJson, filename)) { + return false; + } + List fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class); + if (CollectionUtils.isNotEmpty(fileInfoList)) { + for (UploadResponseInfo fileInfo : fileInfoList) { + if (StringUtils.equals(filename, fileInfo.getFilename())) { + return true; + } + } + } + return false; + } + + protected UploadResponseInfo verifyUploadArgument( + Boolean asImage, MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = new UploadResponseInfo(); + if (Objects.isNull(uploadFile) || uploadFile.isEmpty()) { + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_ARGUMENT.getErrorMessage()); + return responseInfo; + } + if (BooleanUtil.isTrue(asImage) && ImageIO.read(uploadFile.getInputStream()) == null) { + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_FORMAT.getErrorMessage()); + return responseInfo; + } + return responseInfo; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java new file mode 100644 index 00000000..e883d06e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/LocalUpDownloader.java @@ -0,0 +1,169 @@ +package com.orangeforms.common.core.upload; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * 存储本地文件的上传下载实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class LocalUpDownloader extends BaseUpDownloader { + + @Autowired + private UpDownloaderFactory factory; + + @PostConstruct + public void doRegister() { + factory.registerUpDownloader(UploadStoreTypeEnum.LOCAL_SYSTEM, this); + } + + @Override + public void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) { + String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage); + String fullFileanme = uploadPath + "/" + fileName; + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException { + StringBuilder pathBuilder = new StringBuilder(128); + if (StrUtil.isNotBlank(rootBaseDir)) { + pathBuilder.append(rootBaseDir); + } + if (StrUtil.isNotBlank(uriPath)) { + pathBuilder.append(uriPath); + } + pathBuilder.append("/"); + String fullFileanme = pathBuilder.append(fileName).toString(); + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage); + return this.doUploadInternally(serviceContextPath, uploadPath, asImage, uploadFile); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String uriPath, + MultipartFile uploadFile) throws IOException { + String uploadPath = makeFullPath(rootBaseDir, uriPath); + return this.doUploadInternally(serviceContextPath, uploadPath, false, uploadFile); + } + + /** + * 判断filename参数指定的文件名,是否被包含在fileInfoJson参数中。 + * + * @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。 + * @param filename 被包含的文件名。 + * @return 存在返回true,否则false。 + */ + public static boolean containFile(String fileInfoJson, String filename) { + if (StringUtils.isAnyBlank(fileInfoJson, filename)) { + return false; + } + List fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class); + if (CollectionUtils.isNotEmpty(fileInfoList)) { + for (UploadResponseInfo fileInfo : fileInfoList) { + if (StringUtils.equals(filename, fileInfo.getFilename())) { + return true; + } + } + } + return false; + } + + private UploadResponseInfo doUploadInternally( + String serviceContextPath, + String uploadPath, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = super.verifyUploadArgument(asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + return responseInfo; + } + responseInfo.setUploadPath(uploadPath); + fillUploadResponseInfo(responseInfo, serviceContextPath, uploadFile.getOriginalFilename()); + try { + byte[] bytes = uploadFile.getBytes(); + StringBuilder sb = new StringBuilder(256); + sb.append(uploadPath).append("/").append(responseInfo.getFilename()); + Path path = Paths.get(sb.toString()); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(uploadPath)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } catch (IOException e) { + log.error("Failed to write uploaded file [" + uploadFile.getOriginalFilename() + " ].", e); + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_IOERROR.getErrorMessage()); + return responseInfo; + } + return responseInfo; + } + + private void downloadInternal(String fullFileanme, String fileName, HttpServletResponse response) { + File file = new File(fullFileanme); + if (!file.exists()) { + log.warn("Download file [" + fullFileanme + "] failed, no file found!"); + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + byte[] buff = new byte[2048]; + try (OutputStream os = response.getOutputStream(); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { + int i = bis.read(buff); + while (i != -1) { + os.write(buff, 0, i); + os.flush(); + i = bis.read(buff); + } + } catch (IOException e) { + log.error("Failed to call LocalUpDownloader.doDownload", e); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java new file mode 100644 index 00000000..323880d4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UpDownloaderFactory.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.core.upload; + +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.Map; + +/** + * 业务对象根据上传下载存储类型,获取上传下载对象的工厂类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class UpDownloaderFactory { + + private final Map upDownloaderMap = new EnumMap<>(UploadStoreTypeEnum.class); + + /** + * 根据存储类型获取上传下载对象。 + * @param storeType 存储类型。 + * @return 匹配的上传下载对象。 + */ + public BaseUpDownloader get(UploadStoreTypeEnum storeType) { + BaseUpDownloader upDownloader = upDownloaderMap.get(storeType); + if (upDownloader == null) { + throw new UnsupportedOperationException( + "The storeType [" + storeType.name() + "] isn't supported, please add dependency jar first."); + } + return upDownloader; + } + + /** + * 注册上传下载对象到工厂。 + * + * @param storeType 存储类型。 + * @param upDownloader 上传下载对象。 + */ + public void registerUpDownloader(UploadStoreTypeEnum storeType, BaseUpDownloader upDownloader) { + if (storeType == null || upDownloader == null) { + throw new IllegalArgumentException("The Argument can't be NULL."); + } + if (upDownloaderMap.containsKey(storeType)) { + throw new UnsupportedOperationException( + "The storeType [" + storeType.name() + "] has been registered already."); + } + upDownloaderMap.put(storeType, upDownloader); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java new file mode 100644 index 00000000..3610a541 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadResponseInfo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.core.upload; + +import lombok.Data; + +/** + * 数据上传操作的应答信息对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class UploadResponseInfo { + /** + * 上传是否出现错误。 + */ + private Boolean uploadFailed = false; + /** + * 具体错误信息。 + */ + private String errorMessage; + /** + * 返回前端的下载url。 + */ + private String downloadUri; + /** + * 上传文件所在路径。 + */ + private String uploadPath; + /** + * 返回给前端的文件名。 + */ + private String filename; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java new file mode 100644 index 00000000..32d7fed6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreInfo.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.core.upload; + +import lombok.Data; + +/** + * 上传数据存储信息对象。这里之所以使用对象,主要是便于今后扩展。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class UploadStoreInfo { + + /** + * 是否支持上传。 + */ + private boolean supportUpload; + /** + * 上传数据存储类型。 + */ + private UploadStoreTypeEnum storeType; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java new file mode 100644 index 00000000..62c1d2d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/upload/UploadStoreTypeEnum.java @@ -0,0 +1,31 @@ +package com.orangeforms.common.core.upload; + +/** + * 上传数据存储介质类型枚举。 + * + * @author Jerry + * @date 2024-07-02 + */ +public enum UploadStoreTypeEnum { + + /** + * 本地系统。 + */ + LOCAL_SYSTEM, + /** + * minio分布式存储。 + */ + MINIO_SYSTEM, + /** + * 阿里云OSS存储。 + */ + ALIYUN_OSS_SYTEM, + /** + * 腾讯云COS存储。 + */ + QCLOUD_COS_SYTEM, + /** + * 华为云OBS存储。 + */ + HUAWEI_OBS_SYSTEM +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java new file mode 100644 index 00000000..48844678 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/AopTargetUtil.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.ReflectUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * 获取JDK动态代理/CGLIB代理对象代理的目标对象的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class AopTargetUtil { + + /** + * 获取参数对象代理的目标对象。 + * + * @param proxy 代理对象 + * @return 代理的目标对象。 + */ + public static Object getTarget(Object proxy) { + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + try { + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } else { + return getCglibProxyTargetObject(proxy); + } + } catch (Exception e) { + log.error("Failed to call getJdkDynamicProxyTargetObject or getCglibProxyTargetObject", e); + return null; + } + } + + /** + * 获取被织入完整的方法名。 + * + * @param joinPoint 织入方法对象。 + * @return 被织入完整的方法名。 + */ + public static String getFullMethodName(ProceedingJoinPoint joinPoint) { + StringBuilder sb = new StringBuilder(512); + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + sb.append(methodSignature.getMethod().getName()).append("("); + String paramTypes = Arrays.stream(methodSignature.getParameterTypes()) + .map(Class::getSimpleName).collect(Collectors.joining(", ")); + sb.append(paramTypes).append(")"); + return sb.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AopTargetUtil() { + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0"); + Object dynamicAdvisedInterceptor = ReflectUtil.getFieldValue(proxy, h); + Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised"); + return ((AdvisedSupport) ReflectUtil.getFieldValue(dynamicAdvisedInterceptor, advised)).getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + Field h = proxy.getClass().getSuperclass().getDeclaredField("h"); + AopProxy aopProxy = (AopProxy) ReflectUtil.getFieldValue(proxy, h); + Field advised = aopProxy.getClass().getDeclaredField("advised"); + return ((AdvisedSupport) ReflectUtil.getFieldValue(aopProxy, advised)).getTargetSource().getTarget(); + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java new file mode 100644 index 00000000..2a53c923 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ApplicationContextHolder.java @@ -0,0 +1,88 @@ +package com.orangeforms.common.core.util; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * Spring 系统启动应用感知对象,主要用于获取Spring Bean的上下文对象,后续的代码中可以直接查找系统中加载的Bean对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class ApplicationContextHolder implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + /** + * Spring 启动的过程中会自动调用,并将应用上下文对象赋值进来。 + * + * @param applicationContext 应用上下文对象,可通过该对象查找Spring中已经加载的Bean。 + */ + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) { + doSetApplicationContext(applicationContext); + } + + /** + * 获取应用上下文对象。 + * + * @return 应用上下文。 + */ + public static ApplicationContext getApplicationContext() { + assertApplicationContext(); + return applicationContext; + } + + /** + * 根据BeanName,获取Bean对象。 + * + * @param beanName Bean名称。 + * @param 返回的Bean类型。 + * @return Bean对象。 + */ + @SuppressWarnings("unchecked") + public static T getBean(String beanName) { + assertApplicationContext(); + return (T) applicationContext.getBean(beanName); + } + + /** + * 根据Bean的ClassType,获取Bean对象。 + * + * @param beanType Bean的Class类型。 + * @param 返回的Bean类型。 + * @return Bean对象。 + */ + public static T getBean(Class beanType) { + assertApplicationContext(); + return applicationContext.getBean(beanType); + } + + /** + * 根据Bean的ClassType,获取Bean对象列表。 + * + * @param beanType Bean的Class类型。 + * @param 返回的Bean类型。 + * @return Bean对象列表。 + */ + public static Collection getBeanListOfType(Class beanType) { + assertApplicationContext(); + return applicationContext.getBeansOfType(beanType).values(); + } + + private static void assertApplicationContext() { + if (ApplicationContextHolder.applicationContext == null) { + throw new MyRuntimeException("applicaitonContext属性为null,请检查是否注入了ApplicationContextHolder!"); + } + } + + private static void doSetApplicationContext(ApplicationContext applicationContext) { + ApplicationContextHolder.applicationContext = applicationContext; + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java new file mode 100644 index 00000000..95382bde --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ContextUtil.java @@ -0,0 +1,51 @@ +package com.orangeforms.common.core.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 获取Servlet HttpRequest和HttpResponse的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class ContextUtil { + + /** + * 判断当前是否处于HttpServletRequest上下文环境。 + * + * @return 是返回true,否则false。 + */ + public static boolean hasRequestContext() { + return RequestContextHolder.getRequestAttributes() != null; + } + + /** + * 获取Servlet请求上下文的HttpRequest对象。 + * + * @return 请求上下文中的HttpRequest对象。 + */ + public static HttpServletRequest getHttpRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes == null ? null : attributes.getRequest(); + } + + /** + * 获取Servlet请求上下文的HttpResponse对象。 + * + * @return 请求上下文中的HttpResponse对象。 + */ + public static HttpServletResponse getHttpResponse() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes == null ? null : attributes.getResponse(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ContextUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java new file mode 100644 index 00000000..256ddf5a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DataSourceResolver.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.util; + +/** + * 基于自定义解析规则的多数据源解析接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface DataSourceResolver { + + /** + * 动态解析方法。实现类可以根据当前的请求,或者上下文环境进行动态解析。 + * + * @param arg 可选的入参。MyDataSourceResolver注解中的arg参数。 + * @param intArg 可选的整型入参。MyDataSourceResolver注解中的intArg参数。 + * @param methodName 被织入方法名称。 + * @param methodArgs 被织入方法的所有参数。 + * @return 返回用于多数据源切换的类型值。DataSourceResolveAspect 切面方法会根据该返回值和配置信息,进行多数据源切换。 + */ + Integer resolve(String arg, Integer intArg, String methodName, Object[] methodArgs); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java new file mode 100644 index 00000000..b11e16fc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/DefaultDataSourceResolver.java @@ -0,0 +1,55 @@ +package com.orangeforms.common.core.util; + +import org.springframework.stereotype.Component; + +/** + * 常量值指向的数据源。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class DefaultDataSourceResolver implements DataSourceResolver { + + private static final ThreadLocal DEFAULT_CONTEXT_HOLDER = new ThreadLocal<>(); + + @Override + public Integer resolve(String arg, Integer intArg, String methodName, Object[] methodArgs) { + Integer datasourceType = DEFAULT_CONTEXT_HOLDER.get(); + return datasourceType != null ? datasourceType : intArg; + } + + /** + * 设置报表数据源类型值。 + * + * @param type 数据源类型 + * @return 原有数据源类型,如果第一次设置则返回null。 + */ + public static Integer setDataSourceType(Integer type) { + Integer datasourceType = DEFAULT_CONTEXT_HOLDER.get(); + DEFAULT_CONTEXT_HOLDER.set(type); + return datasourceType; + } + + /** + * 获取当前报表数据库操作执行线程的数据源类型,同时由动态数据源的路由函数调用。 + * + * @return 数据源类型。 + */ + public static Integer getDataSourceType() { + return DEFAULT_CONTEXT_HOLDER.get(); + } + + /** + * 清除线程本地变量,以免内存泄漏。 + + * @param originalType 原有的数据源类型,如果该值为null,则情况本地化变量。 + */ + public static void unset(Integer originalType) { + if (originalType == null) { + DEFAULT_CONTEXT_HOLDER.remove(); + } else { + DEFAULT_CONTEXT_HOLDER.set(originalType); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java new file mode 100644 index 00000000..b3d37aa8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ExportUtil.java @@ -0,0 +1,111 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.ExcelWriter; +import cn.jimmyshi.beanquery.BeanQuery; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.MyRuntimeException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.io.FilenameUtils; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 导出工具类,目前支持xlsx和csv两种类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class ExportUtil { + + /** + * 数据导出。目前仅支持xlsx和csv。 + * + * @param dataList 导出数据列表。 + * @param selectFieldMap 导出的数据字段,key为对象字段名称,value为中文标题名称。 + * @param filename 导出文件名。 + * @param 数据对象类型。 + * @throws IOException 文件操作失败。 + */ + public static void doExport( + Collection dataList, Map selectFieldMap, String filename) throws IOException { + if (CollUtil.isEmpty(dataList)) { + return; + } + StringBuilder sb = new StringBuilder(128); + for (Map.Entry e : selectFieldMap.entrySet()) { + sb.append(e.getKey()).append(" as ").append(e.getValue()).append(", "); + } + // 去掉末尾的逗号 + String selectFieldString = sb.substring(0, sb.length() - 2); + // 写出数据到xcel格式的输出流 + List> resultList = BeanQuery.select(selectFieldString).executeFrom(dataList); + normalizeMultiSelectList(resultList); + // 构建HTTP输出流参数 + HttpServletResponse response = ContextUtil.getHttpResponse(); + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + filename); + if (ApplicationConstant.XLSX_EXT.equals(FilenameUtils.getExtension(filename))) { + ServletOutputStream out = response.getOutputStream(); + ExcelWriter writer = ExcelUtil.getWriter(true); + writer.setRowHeight(-1, 30); + writer.setColumnWidth(-1, 30); + writer.setColumnWidth(1, 20); + writer.write(resultList); + writer.flush(out); + writer.close(); + IoUtil.close(out); + } else if (ApplicationConstant.CSV_EXT.equals(FilenameUtils.getExtension(filename))) { + Collection headerList = selectFieldMap.values(); + String[] headerArray = new String[headerList.size()]; + headerList.toArray(headerArray); + CSVFormat format = CSVFormat.DEFAULT.withHeader(headerArray); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + try (Writer out = response.getWriter(); CSVPrinter printer = new CSVPrinter(out, format)) { + for (Map o : resultList) { + for (Map.Entry entry : o.entrySet()) { + printer.print(entry.getValue()); + } + printer.println(); + } + printer.flush(); + } catch (Exception e) { + log.error("Failed to call ExportUtil.doExport", e); + } + } else { + throw new MyRuntimeException("不支持的导出文件类型!"); + } + } + + @SuppressWarnings("unchecked") + private static void normalizeMultiSelectList(List> resultList) { + for (Map data : resultList) { + for (Map.Entry entry : data.entrySet()) { + if (entry.getValue() instanceof List) { + List> dictMapList = ((List>) entry.getValue()); + List nameList = dictMapList.stream() + .map(item -> item.get("name").toString()).collect(Collectors.toList()); + data.put(entry.getKey(), CollUtil.join(nameList, ",")); + } + } + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ExportUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java new file mode 100644 index 00000000..baa79c78 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/ImportUtil.java @@ -0,0 +1,352 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.sax.handler.RowHandler; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationDict; +import com.orangeforms.common.core.annotation.RelationGlobalDict; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.exception.MyRuntimeException; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 导入工具类,目前支持xlsx和xls两种类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class ImportUtil { + + /** + * 根据实体类的Class类型,生成导入的头信息。 + * + * @param modelClazz 实体对象的Class类型。 + * @param ignoreFields 忽略的字段名集合,如创建时间、创建人、更新时间、更新人等。 + * @param 实体对象类型。 + * @return 创建后的导入头信息列表。 + */ + public static List makeHeaderInfoList(Class modelClazz, Set ignoreFields) { + List resultList = new LinkedList<>(); + Field[] fields = ReflectUtil.getFields(modelClazz); + int index = 0; + for (Field field : fields) { + int modifiers = field.getModifiers(); + // transient类型的字段不能作为查询条件,静态字段和逻辑删除都不考虑。需要忽略的字段也要跳过。 + int transientMask = 128; + if ((modifiers & transientMask) == 1 + || Modifier.isStatic(modifiers) + || field.getAnnotation(TableId.class) != null + || field.getAnnotation(TableLogic.class) != null + || CollUtil.contains(ignoreFields, field.getName())) { + continue; + } + TableField tableField = field.getAnnotation(TableField.class); + if (tableField == null || tableField.exist()) { + ImportHeaderInfo headerInfo = new ImportHeaderInfo(); + headerInfo.fieldName = field.getName(); + headerInfo.index = index++; + makeHeaderInfoFieldTypeByField(field, headerInfo); + resultList.add(headerInfo); + } + } + return resultList; + } + + /** + * 保存导入文件。 + * + * @param baseDir 导入文件本地缓存的根目录。 + * @param subDir 导入文件本地缓存的子目录。 + * @param importFile 导入的文件。 + * @return 保存的本地文件名。 + */ + public static String saveImportFile( + String baseDir, String subDir, MultipartFile importFile) throws IOException { + StringBuilder sb = new StringBuilder(256); + sb.append(baseDir); + if (!StrUtil.endWith(baseDir, "/")) { + sb.append("/"); + } + sb.append("importedFile/"); + if (StrUtil.isNotBlank(subDir)) { + sb.append(subDir); + if (!StrUtil.endWith(subDir, "/")) { + sb.append("/"); + } + } + String pathname = sb.toString(); + sb.append(new DateTime().toString("yyyy-MM-dd-HH-mm-")); + sb.append(MyCommonUtil.generateUuid()) + .append(".").append(FileNameUtil.getSuffix(importFile.getOriginalFilename())); + String fullname = sb.toString(); + try { + byte[] bytes = importFile.getBytes(); + Path path = Paths.get(fullname); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(pathname)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } catch (IOException e) { + log.error("Failed to write imported file [" + importFile.getOriginalFilename() + " ].", e); + throw e; + } + return fullname; + } + + /** + * 导入指定的excel,基于SAX方式解析后返回数据列表。 + * + * @param headers 头信息数组。 + * @param skipHeader 是否跳过第一行,通常改行为头信息。 + * @param filename 文件名。 + * @return 解析后数据列表。 + */ + public static List> doImport( + ImportHeaderInfo[] headers, boolean skipHeader, String filename) { + Assert.notNull(headers); + Assert.isTrue(StrUtil.isNotBlank(filename)); + List> resultList = new LinkedList<>(); + ExcelUtil.readBySax(new File(filename), 0, createRowHandler(headers, skipHeader, resultList)); + return resultList; + } + + /** + * 导入指定的excel,基于SAX方式解析后返回Bean类型的数据列表。 + * + * @param headers 头信息数组。 + * @param skipHeader 是否跳过第一行,通常改行为头信息。 + * @param filename 文件名。 + * @param clazz Bean的Class类型。 + * @param translateDictFieldSet 需要进行反向翻译的字典字段集合。 + * @return 解析后数据列表。 + */ + public static List doImport( + ImportHeaderInfo[] headers, + boolean skipHeader, + String filename, + Class clazz, + Set translateDictFieldSet) { + // 这里将需要进行字典反向翻译的字段类型改为String,否则使用原有的字典Id类型时,无法正确执行下面的doImport方法。 + if (CollUtil.isNotEmpty(translateDictFieldSet)) { + for (ImportHeaderInfo header : headers) { + if (translateDictFieldSet.contains(header.fieldName)) { + header.fieldType = STRING_TYPE; + } + } + } + List> resultList = doImport(headers, skipHeader, filename); + if (CollUtil.isNotEmpty(translateDictFieldSet)) { + translateDictFieldSet.forEach(c -> doTranslateDict(resultList, clazz, c)); + } + return MyModelUtil.mapToBeanList(resultList, clazz); + } + + /** + * 转换数据列表中,需要进行反向字典翻译的字段。 + * + * @param dataList 数据列表。 + * @param modelClass 对象模型。 + * @param fieldName 需要进行字典反向翻译的字段名。注意,该字段为需要翻译替换的Java字段名,与此同时, + * 该字段 + DictMap后缀的字段名,必须被RelationConstDict和RelationDict注解标记。 + */ + @SuppressWarnings("unchecked") + public static void doTranslateDict(List> dataList, Class modelClass, String fieldName) { + if (CollUtil.isEmpty(dataList)) { + return; + } + Field field = ReflectUtil.getField(modelClass, fieldName + "DictMap"); + Assert.notNull(field); + Map inversedDictMap; + if (field.isAnnotationPresent(RelationConstDict.class)) { + RelationConstDict r = field.getAnnotation(RelationConstDict.class); + Field f = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = (Map) ReflectUtil.getStaticFieldValue(f); + inversedDictMap = MapUtil.inverse(dictMap); + } else if (field.isAnnotationPresent(RelationDict.class)) { + RelationDict r = field.getAnnotation(RelationDict.class); + String slaveServiceName = r.slaveServiceName(); + if (StrUtil.isBlank(slaveServiceName)) { + slaveServiceName = r.slaveModelClass().getSimpleName() + "Service"; + } + BaseService service = + ApplicationContextHolder.getBean(StrUtil.lowerFirst(slaveServiceName)); + List dictDataList = service.getAllList(); + List> dataMapList = MyModelUtil.beanToMapList(dictDataList); + inversedDictMap = new HashMap<>(dataMapList.size()); + dataMapList.forEach(d -> + inversedDictMap.put(d.get(r.slaveNameField()).toString(), d.get(r.slaveIdField()))); + } else if (field.isAnnotationPresent(RelationGlobalDict.class)) { + RelationGlobalDict r = field.getAnnotation(RelationGlobalDict.class); + BaseService s = ApplicationContextHolder.getBean("globalDictService"); + Method m = ReflectUtil.getMethodByName(s.getClass(), "getGlobalDictItemDictMapFromCache"); + Map dictMap = ReflectUtil.invoke(s, m, r.dictCode(), null); + inversedDictMap = MapUtil.inverse(dictMap); + } else { + throw new UnsupportedOperationException("Only Support RelationConstDict and RelationDict Field"); + } + if (MapUtil.isEmpty(inversedDictMap)) { + log.warn("Dict Data List is EMPTY."); + return; + } + for (Map data : dataList) { + Object value = data.get(fieldName); + if (value != null) { + Object newValue = inversedDictMap.get(value.toString()); + if (newValue != null) { + data.put(fieldName, newValue); + } + } + } + } + + private static void makeHeaderInfoFieldTypeByField(Field field, ImportHeaderInfo headerInfo) { + if (field.getType().equals(Integer.class)) { + headerInfo.fieldType = INT_TYPE; + } else if (field.getType().equals(Long.class)) { + headerInfo.fieldType = LONG_TYPE; + } else if (field.getType().equals(String.class)) { + headerInfo.fieldType = STRING_TYPE; + } else if (field.getType().equals(Boolean.class)) { + headerInfo.fieldType = BOOLEAN_TYPE; + } else if (field.getType().equals(Date.class)) { + headerInfo.fieldType = DATE_TYPE; + } else if (field.getType().equals(Double.class)) { + headerInfo.fieldType = DOUBLE_TYPE; + } else if (field.getType().equals(Float.class)) { + headerInfo.fieldType = FLOAT_TYPE; + } else if (field.getType().equals(BigDecimal.class)) { + headerInfo.fieldType = BIG_DECIMAL_TYPE; + } else { + throw new MyRuntimeException("Unsupport Import FieldType"); + } + } + + private static RowHandler createRowHandler( + ImportHeaderInfo[] headers, boolean skipHeader, List> resultList) { + return new MyRowHandler(headers, skipHeader, resultList); + } + + public static final int INT_TYPE = 0; + public static final int LONG_TYPE = 1; + public static final int STRING_TYPE = 2; + public static final int BOOLEAN_TYPE = 3; + public static final int DATE_TYPE = 4; + public static final int DOUBLE_TYPE = 5; + public static final int FLOAT_TYPE = 6; + public static final int BIG_DECIMAL_TYPE = 7; + + @NoArgsConstructor + @AllArgsConstructor + @Data + public static class ImportHeaderInfo { + /** + * 对应的Java实体对象属性名。 + */ + private String fieldName; + /** + * 对应的Java实体对象类型。 + */ + private Integer fieldType; + /** + * 0 表示excel中的第一列。 + */ + private Integer index; + } + + private static class MyRowHandler implements RowHandler { + private ImportHeaderInfo[] headers; + private Map headerInfoMap; + private boolean skipHeader; + private List> resultList; + + public MyRowHandler(ImportHeaderInfo[] headers, boolean skipHeader, List> resultList) { + this.headers = headers; + this.skipHeader = skipHeader; + this.resultList = resultList; + this.headerInfoMap = Arrays.stream(headers) + .collect(Collectors.toMap(ImportHeaderInfo::getIndex, c -> c)); + } + + @Override + public void handle(int sheetIndex, long rowIndex, List rowList) { + if (this.skipHeader && rowIndex == 0) { + return; + } + int i = 0; + Map data = new HashMap<>(headers.length); + for (Object rowData : rowList) { + ImportHeaderInfo headerInfo = this.headerInfoMap.get(i++); + if (headerInfo == null) { + continue; + } + switch (headerInfo.fieldType) { + case INT_TYPE: + data.put(headerInfo.fieldName, Convert.toInt(rowData)); + break; + case LONG_TYPE: + data.put(headerInfo.fieldName, Convert.toLong(rowData)); + break; + case STRING_TYPE: + data.put(headerInfo.fieldName, Convert.toStr(rowData)); + break; + case BOOLEAN_TYPE: + data.put(headerInfo.fieldName, Convert.toBool(rowData)); + break; + case DATE_TYPE: + data.put(headerInfo.fieldName, Convert.toDate(rowData)); + break; + case DOUBLE_TYPE: + data.put(headerInfo.fieldName, Convert.toDouble(rowData)); + break; + case FLOAT_TYPE: + data.put(headerInfo.fieldName, Convert.toFloat(rowData)); + break; + case BIG_DECIMAL_TYPE: + data.put(headerInfo.fieldName, Convert.toBigDecimal(rowData)); + break; + default: + throw new MyRuntimeException( + "Invalid ImportHeaderInfo.fieldType [" + headerInfo.fieldType + "]."); + } + } + resultList.add(data); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ImportUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java new file mode 100644 index 00000000..c9ac471f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/IpUtil.java @@ -0,0 +1,104 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * Ip工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class IpUtil { + + private static final String UNKNOWN = "unknown"; + + /** + * 通过Servlet的HttpRequest对象获取Ip地址。 + * + * @param request HttpRequest对象。 + * @return 本次请求的Ip地址。 + */ + public static String getRemoteIpAddress(HttpServletRequest request) { + String ip = null; + // X-Forwarded-For:Squid 服务代理 + String ipAddresses = request.getHeader("X-Forwarded-For"); + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // Proxy-Client-IP:apache 服务代理 + ipAddresses = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // WL-Proxy-Client-IP:weblogic 服务代理 + ipAddresses = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // HTTP_CLIENT_IP:有些代理服务器 + ipAddresses = request.getHeader("HTTP_CLIENT_IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // X-Real-IP:nginx服务代理 + ipAddresses = request.getHeader("X-Real-IP"); + } + // 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP + if (StrUtil.isNotBlank(ipAddresses)) { + ip = ipAddresses.split(",")[0]; + } + // 还是不能获取到,最后再通过request.getRemoteAddr();获取 + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + public static String getFirstLocalIpAddress() { + String ip; + try { + List ipList = getHostAddress(); + // default the first + ip = (!ipList.isEmpty()) ? ipList.get(0) : ""; + } catch (Exception ex) { + ip = ""; + log.error("Failed to call ", ex); + } + return ip; + } + + private static List getHostAddress() throws SocketException { + List ipList = new ArrayList<>(5); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + Enumeration allAddress = ni.getInetAddresses(); + while (allAddress.hasMoreElements()) { + InetAddress address = allAddress.nextElement(); + // skip the IPv6 addr + // skip the IPv6 addr + if (address.isLoopbackAddress() || address instanceof Inet6Address) { + continue; + } + String hostAddress = address.getHostAddress(); + ipList.add(hostAddress); + } + } + return ipList; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private IpUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java new file mode 100644 index 00000000..84e23a06 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/JwtUtil.java @@ -0,0 +1,112 @@ +package com.orangeforms.common.core.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.Map; + +/** + * 基于JWT的Token生成工具类 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class JwtUtil { + + private static final String TOKEN_PREFIX = "Bearer "; + private static final String CLAIM_KEY_CREATEDTIME = "CreatedTime"; + + /** + * Token缺省过期时间是30分钟 + */ + private static final Long TOKEN_EXPIRATION = 1800000L; + /** + * 缺省情况下,Token会每5分钟被刷新一次 + */ + private static final Long REFRESH_TOKEN_INTERVAL = 300000L; + + /** + * 生成加密后的JWT令牌,生成的结果中包含令牌前缀,如"Bearer " + * + * @param claims 令牌中携带的数据 + * @param expirationMillisecond 过期的毫秒数 + * @return 生成后的令牌信息 + */ + public static String generateToken(Map claims, long expirationMillisecond, String signingKey) { + // 自动添加token的创建时间 + long createTime = System.currentTimeMillis(); + claims.put(CLAIM_KEY_CREATEDTIME, createTime); + SecretKey sk = Keys.hmacShaKeyFor(signingKey.getBytes()); + String token = Jwts.builder().claims(claims) + .signWith(sk, Jwts.SIG.HS256) + .expiration(new Date(createTime + expirationMillisecond)) + .compact(); + return TOKEN_PREFIX + token; + } + + /** + * 生成加密后的JWT令牌,生成的结果中包含令牌前缀,如"Bearer " + * + * @param claims 令牌中携带的数据 + * @return 生成后的令牌信息 + */ + public static String generateToken(Map claims, String signingKey) { + return generateToken(claims, TOKEN_EXPIRATION, signingKey); + } + + /** + * 获取token中的数据对象 + * + * @param token 令牌信息(需要包含令牌前缀,如"Bearer ") + * @return 令牌中的数据对象,解析视频返回null。 + */ + public static Claims parseToken(String token, String signingKey) { + if (token == null || !token.startsWith(TOKEN_PREFIX)) { + return null; + } + String tokenKey = token.substring(TOKEN_PREFIX.length()); + Claims claims = null; + try { + SecretKey sk = Keys.hmacShaKeyFor(signingKey.getBytes()); + claims = Jwts.parser().verifyWith(sk).build().parseSignedClaims(tokenKey).getPayload(); + } catch (Exception e) { + log.error("Token Expired", e); + } + return claims; + } + + /** + * 判断令牌是否过期 + * + * @param claims 令牌解密后的Map对象。 + * @return true 过期,否则false。 + */ + public static boolean isNullOrExpired(Claims claims) { + return claims == null || claims.getExpiration().before(new Date()); + } + + /** + * 判断解密后的Token payload是否需要被强制刷新,如果需要,则调用generateToken方法重新生成Token。 + * + * @param claims Token解密后payload数据 + * @return true 需要刷新,否则false + */ + public static boolean needToRefresh(Claims claims) { + if (claims == null) { + return false; + } + Long createTime = (Long) claims.get(CLAIM_KEY_CREATEDTIME); + return createTime == null || System.currentTimeMillis() - createTime > REFRESH_TOKEN_INTERVAL; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private JwtUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java new file mode 100644 index 00000000..b89dd09b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/LogMessageUtil.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.core.util; + +/** + * 拼接日志消息的工具类。 + * 主要目标是,尽量保证日志输出的统一性,同时也可以有效减少与日志信息相关的常量字符串, + * 提高代码的规范度和可维护性。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class LogMessageUtil { + + /** + * RPC调用错误格式。 + */ + private static final String RPC_ERROR_MSG_FORMAT = "RPC Failed with Error message [%s]"; + + /** + * 组装RPC调用的错误信息。 + * + * @param errorMsg 具体的错误信息。 + * @return 格式化后的错误信息。 + */ + public static String makeRpcError(String errorMsg) { + return String.format(RPC_ERROR_MSG_FORMAT, errorMsg); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private LogMessageUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java new file mode 100644 index 00000000..e1d3bc4b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldHandler.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.core.util; + +/** + * 自定义脱敏处理器接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface MaskFieldHandler { + + /** + * 处理自定义的脱敏数据。可以根据表名和字段名,使用不同的自定义脱敏规则。 + * + * @param modelName 脱敏字段所在实体对象名。 + * @param fieldName 脱敏实体对象名中的字段属性名。 + * @param data 待脱敏的数据。 + * @param maskChar 脱敏掩码字符。 + * @return 脱敏后的数据。 + */ + String handleMask(String modelName, String fieldName, String data, char maskChar); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java new file mode 100644 index 00000000..830aa2ff --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MaskFieldUtil.java @@ -0,0 +1,203 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 脱敏的工具类。具体实现的源码基本来自hutool的DesensitizedUtil, + * 只是因为我们需要支持自定义脱敏字符,因此需要重写hutool中的工具类方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MaskFieldUtil { + + /** + * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**。 + * + * @param fullName 姓名。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的姓名。 + */ + public static String chineseName(String fullName, char maskChar) { + if (StrUtil.isBlank(fullName)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(fullName, 1, fullName.length(), maskChar); + } + + /** + * 【身份证号】前1位 和后2位。 + * + * @param idCardNum 身份证。 + * @param front 保留:前面的front位数;从1开始。 + * @param end 保留:后面的end位数;从1开始。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的身份证。 + */ + public static String idCardNum(String idCardNum, int front, int end, char maskChar) { + return noMaskPrefixAndSuffix(idCardNum, front, end, maskChar); + } + + /** + * 字符串的前front位和后end位的字符,不会被脱敏。 + * + * @param str 原字符串。 + * @param front 保留:前面的front位数;从1开始。 + * @param end 保留:后面的end位数;从1开始。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的结果字符串。 + */ + public static String noMaskPrefixAndSuffix(String str, int front, int end, char maskChar) { + //身份证不能为空 + if (StrUtil.isBlank(str)) { + return StrUtil.EMPTY; + } + //需要截取的长度不能大于身份证号长度 + if ((front + end) > str.length()) { + return StrUtil.EMPTY; + } + //需要截取的不能小于0 + if (front < 0 || end < 0) { + return StrUtil.EMPTY; + } + return StrUtil.replace(str, front, str.length() - end, maskChar); + } + + /** + * 【固定电话 前四位,后两位。 + * + * @param num 固定电话。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的固定电话。 + */ + public static String fixedPhone(String num, char maskChar) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(num, 4, num.length() - 2, maskChar); + } + + /** + * 【手机号码】前三位,后4位,其他隐藏,比如135****2210。 + * + * @param num 移动电话。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的移动电话。 + */ + public static String mobilePhone(String num, char maskChar) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(num, 3, num.length() - 4, maskChar); + } + + /** + * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****。 + * + * @param address 家庭住址。 + * @param sensitiveSize 敏感信息长度。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的家庭地址。 + */ + public static String address(String address, int sensitiveSize, char maskChar) { + if (StrUtil.isBlank(address)) { + return StrUtil.EMPTY; + } + int length = address.length(); + return StrUtil.replace(address, length - sensitiveSize, length, maskChar); + } + + /** + * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com。 + * + * @param email 邮箱。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的邮箱。 + */ + public static String email(String email, char maskChar) { + if (StrUtil.isBlank(email)) { + return StrUtil.EMPTY; + } + int index = StrUtil.indexOf(email, '@'); + if (index <= 1) { + return email; + } + return StrUtil.replace(email, 1, index, maskChar); + } + + /** + * 【密码】密码的全部字符都用*代替,比如:******。 + * + * @param password 密码。 + * @return 脱敏后的密码。 + */ + public static String password(String password) { + if (StrUtil.isBlank(password)) { + return StrUtil.EMPTY; + } + return StrUtil.repeat('*', password.length()); + } + + /** + * 【中国车牌】车牌中间用*代替。 + * eg1:null -》 "" + * eg1:"" -》 "" + * eg3:苏D40000 -》 苏D4***0 + * eg4:陕A12345D -》 陕A1****D + * eg5:京A123 -》 京A123 如果是错误的车牌,不处理。 + * + * @param carLicense 完整的车牌号。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的车牌。 + */ + public static String carLicense(String carLicense, char maskChar) { + if (StrUtil.isBlank(carLicense)) { + return StrUtil.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) { + carLicense = StrUtil.replace(carLicense, 3, 6, maskChar); + } else if (carLicense.length() == 8) { + // 新能源车牌 + carLicense = StrUtil.replace(carLicense, 3, 7, maskChar); + } + return carLicense; + } + + /** + * 银行卡号脱敏。 + * eg: 1101 **** **** **** 3256。 + * + * @param bankCardNo 银行卡号。 + * @param maskChar 遮掩字符。 + * @return 脱敏之后的银行卡号。 + */ + public static String bankCard(String bankCardNo, char maskChar) { + if (StrUtil.isBlank(bankCardNo)) { + return bankCardNo; + } + bankCardNo = StrUtil.trim(bankCardNo); + if (bankCardNo.length() < 9) { + return bankCardNo; + } + final int length = bankCardNo.length(); + final int midLength = length - 8; + final StringBuilder buf = new StringBuilder(); + buf.append(bankCardNo, 0, 4); + for (int i = 0; i < midLength; ++i) { + if (i % 4 == 0) { + buf.append(CharUtil.SPACE); + } + buf.append(maskChar); + } + buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); + return buf.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MaskFieldUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java new file mode 100644 index 00000000..fa97c514 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCommonUtil.java @@ -0,0 +1,442 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.orangeforms.common.core.constant.AppDeviceType; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.groups.Default; +import java.lang.reflect.Field; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 脚手架中常用的基本工具方法集合,一般而言工程内部使用的方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyCommonUtil { + + private static final Validator VALIDATOR; + + static { + VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + } + + /** + * 创建uuid。 + * + * @return 返回uuid。 + */ + public static String generateUuid() { + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 对用户密码进行加盐后加密。 + * + * @param password 明文密码。 + * @param passwordSalt 盐值。 + * @return 加密后的密码。 + */ + public static String encrptedPassword(String password, String passwordSalt) { + return DigestUtil.md5Hex(password + passwordSalt); + } + + /** + * 这个方法一般用于Controller对于入口参数的基本验证。 + * 对于字符串,如果为空字符串,也将视为Blank,同时返回true。 + * + * @param objs 一组参数。 + * @return 返回是否存在null或空字符串的参数。 + */ + public static boolean existBlankArgument(Object...objs) { + for (Object obj : objs) { + if (MyCommonUtil.isBlankOrNull(obj)) { + return true; + } + } + return false; + } + + /** + * 结果和 existBlankArgument 相反。 + * + * @param objs 一组参数。 + * @return 返回是否存在null或空字符串的参数。 + */ + public static boolean existNotBlankArgument(Object...objs) { + for (Object obj : objs) { + if (!MyCommonUtil.isBlankOrNull(obj)) { + return true; + } + } + return false; + } + + /** + * 验证参数是否为空。 + * + * @param obj 待判断的参数。 + * @return 空或者null返回true,否则false。 + */ + public static boolean isBlankOrNull(Object obj) { + if (obj instanceof Collection) { + return CollUtil.isEmpty((Collection) obj); + } + return obj == null || (obj instanceof CharSequence && StrUtil.isBlank((CharSequence) obj)); + } + + /** + * 验证参数是否为非空。 + * + * @param obj 待判断的参数。 + * @return 空或者null返回false,否则true。 + */ + public static boolean isNotBlankOrNull(Object obj) { + return !isBlankOrNull(obj); + } + + /** + * 判断source是否等于其中任何一个对象值。 + * + * @param source 源对象。 + * @param others 其他对象。 + * @return 等于其中任何一个返回true,否则false。 + */ + public static boolean equalsAny(Object source, Object...others) { + for (Object one : others) { + if (ObjectUtil.equal(source, one)) { + return true; + } + } + return false; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param model 带校验的model。 + * @param groups Validate绑定的校验组。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(T model, Class...groups) { + if (model != null) { + Set> constraintViolations = VALIDATOR.validate(model, groups); + if (!constraintViolations.isEmpty()) { + Iterator> it = constraintViolations.iterator(); + ConstraintViolation constraint = it.next(); + return constraint.getMessage(); + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param model 带校验的model。 + * @param forUpdate 是否为更新。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(T model, boolean forUpdate) { + if (model != null) { + Set> constraintViolations; + if (forUpdate) { + constraintViolations = VALIDATOR.validate(model, Default.class, UpdateGroup.class); + } else { + constraintViolations = VALIDATOR.validate(model, Default.class, AddGroup.class); + } + if (!constraintViolations.isEmpty()) { + Iterator> it = constraintViolations.iterator(); + ConstraintViolation constraint = it.next(); + return constraint.getMessage(); + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param modelList 带校验的model列表。 + * @param groups Validate绑定的校验组。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(List modelList, Class... groups) { + if (CollUtil.isNotEmpty(modelList)) { + for (T model : modelList) { + String errorMessage = getModelValidationError(model, groups); + if (StrUtil.isNotBlank(errorMessage)) { + return errorMessage; + } + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param modelList 带校验的model列表。 + * @param forUpdate 是否为更新。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(List modelList, boolean forUpdate) { + if (CollUtil.isNotEmpty(modelList)) { + for (T model : modelList) { + String errorMessage = getModelValidationError(model, forUpdate); + if (StrUtil.isNotBlank(errorMessage)) { + return errorMessage; + } + } + } + return null; + } + + /** + * 拼接参数中的字符串列表,用指定分隔符进行分割,同时每个字符串对象用单引号括起来。 + * + * @param dataList 字符串集合。 + * @param separator 分隔符。 + * @return 拼接后的字符串。 + */ + public static String joinString(Collection dataList, final char separator) { + int index = 0; + StringBuilder sb = new StringBuilder(128); + for (String data : dataList) { + sb.append("'").append(data).append("'"); + if (index++ != dataList.size() - 1) { + sb.append(separator); + } + } + return sb.toString(); + } + + /** + * 将SQL Like中的通配符替换为字符本身的含义,以便于比较。 + * + * @param str 待替换的字符串。 + * @return 替换后的字符串。 + */ + public static String replaceSqlWildcard(String str) { + if (StrUtil.isBlank(str)) { + return str; + } + return StrUtil.replaceChars(StrUtil.replaceChars(str, "_", "\\_"), "%", "\\%"); + } + + /** + * 获取对象中,非空字段的名字列表。 + * + * @param object 数据对象。 + * @param clazz 数据对象的class类型。 + * @param 数据对象类型。 + * @return 数据对象中,值不为NULL的字段数组。 + */ + public static String[] getNotNullFieldNames(T object, Class clazz) { + Field[] fields = ReflectUtil.getFields(clazz); + List fieldNameList = Arrays.stream(fields) + .filter(f -> ReflectUtil.getFieldValue(object, f) != null) + .map(Field::getName).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(fieldNameList)) { + return fieldNameList.toArray(new String[]{}); + } + return new String[]{}; + } + + /** + * 获取请求头中的设备信息。 + * + * @return 设备类型,具体值可参考AppDeviceType常量类。 + */ + public static int getDeviceType() { + // 缺省都按照Web登录方式设置,如果前端header中的值为不合法值,这里也不会报错,而是使用Web缺省方式。 + int deviceType = AppDeviceType.WEB; + String deviceTypeString = ContextUtil.getHttpRequest().getHeader("deviceType"); + if (StrUtil.isNotBlank(deviceTypeString)) { + Integer type = Integer.valueOf(deviceTypeString); + if (AppDeviceType.isValid(type)) { + deviceType = type; + } + } + return deviceType; + } + + /** + * 获取请求头中的设备信息。 + * + * @return 设备类型,具体值可参考AppDeviceType常量类。 + */ + public static String getDeviceTypeWithString() { + // 缺省都按照Web登录方式设置,如果前端header中的值为不合法值,这里也不会报错,而是使用Web缺省方式。 + int deviceType = AppDeviceType.WEB; + String deviceTypeString = ContextUtil.getHttpRequest().getHeader("deviceType"); + if (StrUtil.isNotBlank(deviceTypeString)) { + Integer type = Integer.valueOf(deviceTypeString); + if (AppDeviceType.isValid(type)) { + deviceType = type; + } + } + return AppDeviceType.getDeviceTypeName(deviceType); + } + + /** + * 获取第三方应用的编码。 + * + * @return 第三方应用编码。 + */ + public static String getAppCodeFromRequest() { + HttpServletRequest request = ContextUtil.getHttpRequest(); + String appCode = request.getHeader("AppCode"); + if (StrUtil.isBlank(appCode)) { + appCode = request.getParameter("AppCode"); + } + return appCode; + } + + /** + * 获取用户身份令牌。 + * + * @param tokenKey 令牌的Key。 + * @return 用户身份令牌。 + */ + public static String getTokenFromRequest(String tokenKey) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + String token = request.getHeader(tokenKey); + if (StrUtil.isBlank(token)) { + token = request.getParameter(tokenKey); + } + if (StrUtil.isBlank(token)) { + token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN); + } + return token; + } + + /** + * 转换为字典格式的数据列表。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, Function idGetter, Function nameGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 转换为树形字典格式的数据列表。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param parentIdGetter 获取字典Id父字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, + Function idGetter, + Function nameGetter, + Function parentIdGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + dataMap.put(ApplicationConstant.PARENT_ID, parentIdGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 转换为字典格式的数据列表,同时支持一个附加字段。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param extraName 附加字段名。。 + * @param extraGetter 获取附加字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @param 附加字段值的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, + Function idGetter, + Function nameGetter, + String extraName, + Function extraGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + dataMap.put(extraName, extraGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 将SQL查询条件中的变量值替换为SQL拼接的字符串值。 + * + * @param value 参数值。 + * @return 转换后的参数字符串。 + */ + public static String convertSqlParamValue(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof Number) { + return String.valueOf(value); + } + if (value instanceof Boolean) { + return String.valueOf(value.equals(Boolean.TRUE) ? 1 : 0); + } + StringBuilder builder = new StringBuilder(); + builder.append("'"); + if (value instanceof Date) { + builder.append(DateUtil.format((Date) value, MyDateUtil.COMMON_SHORT_DATETIME_FORMAT)); + } else if (value instanceof String) { + builder.append(value); + } + builder.append("'"); + return builder.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyCommonUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java new file mode 100644 index 00000000..3f4c2c1a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyCustomMaskFieldHandler.java @@ -0,0 +1,23 @@ +package com.orangeforms.common.core.util; + +import org.springframework.stereotype.Component; + +/** + * 缺省的自定义脱敏处理器的实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class MyCustomMaskFieldHandler implements MaskFieldHandler { + + @Override + public String handleMask(String modelName, String fieldName, String data, char maskChar) { + // 这里是我们默认提供的躺平实现方式。 + // 在默认生成的代码中,如果脱敏字段的处理类型为CUSTOM的时候,就会暂时使用 + // 该类为默认实现,其实这里就是一个占位符实现类。用户可根据需求自行实现自己所需的脱敏处理器实现类。 + // 实现后,可在脱敏字段的MaskField注解的handler参数中,改为自己的实现类。 + // 最后一句很重要,实现类必须是bean对象,如当前类用@Component注解标记。 + throw new UnsupportedOperationException("请仔细阅读上面的代码注解,并实现自己的处理类,以替代默认生成的自定义实现类!!"); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java new file mode 100644 index 00000000..033c5178 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyDateUtil.java @@ -0,0 +1,320 @@ +package com.orangeforms.common.core.util; + +import com.orangeforms.common.core.object.Tuple2; +import org.apache.commons.lang3.time.DateUtils; +import org.joda.time.DateTime; +import org.joda.time.Period; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import java.util.Calendar; +import java.util.Date; + +import static org.joda.time.PeriodType.days; + +/** + * 日期工具类,主要封装了部分joda-time中的方法,让很多代码一行完成,同时统一了日期到字符串的pattern格式。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyDateUtil { + + /** + * 统一的日期pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_DATE_FORMAT = "yyyy-MM-dd"; + /** + * 统一的日期时间pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + /** + * 统一的短日期时间pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_SHORT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + /** + * 缺省日期格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATE_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_DATE_FORMAT); + /** + * 缺省日期时间格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATETIME_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_DATETIME_FORMAT); + + /** + * 缺省短日期时间格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATETIME_SHORT_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + + /** + * 获取一天的开始时间的字符串格式,如2019-08-03 00:00:00.000。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginTimeOfDay(DateTime dateTime) { + return dateTime.withTimeAtStartOfDay().toString(COMMON_DATETIME_FORMAT); + } + + /** + * 获取一天的结束时间的字符串格式,如2019-08-03 23:59:59.999。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndTimeOfDay(DateTime dateTime) { + return dateTime.withTime(23, 59, 59, 999).toString(COMMON_DATETIME_FORMAT); + } + + /** + * 获取一天的开始时间的字符串短格式,如2019-08-03 00:00:00。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginTimeOfDayWithShort(DateTime dateTime) { + return dateTime.withTimeAtStartOfDay().toString(COMMON_SHORT_DATETIME_FORMAT); + } + + /** + * 获取一天的结束时间的字符串短格式,如2019-08-03 23:59:59。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndTimeOfDayWithShort(DateTime dateTime) { + return dateTime.withTime(23, 59, 59, 999).toString(COMMON_SHORT_DATETIME_FORMAT); + } + + /** + * 获取参数时间对象所在周的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfWeek(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfWeek().withMinimumValue()); + } + + /** + * 获取参数时间对象所在周的结束时间的字符串短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfWeek(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfWeek().withMaximumValue()); + } + + /** + * 获取参数时间对象所在月份第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfMonth(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfMonth().withMinimumValue()); + } + + /** + * 获取参数时间对象所在月份的结束时间的字符串短格式, + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfMonth(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfMonth().withMaximumValue()); + } + + /** + * 获取参数时间对象所在年的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfYear(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfYear().withMinimumValue()); + } + + /** + * 获取参数时间对象所在年的结束时间的字符串短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfYear(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfYear().withMaximumValue()); + } + + + /** + * 获取参数时间对象所在季度的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfQuarter(DateTime dateTime) { + int m = dateTime.getMonthOfYear(); + int m2 = 10; + if (m >= 1 && m <= 3) { + m2 = 1; + } else if (m >= 4 && m <= 6) { + m2 = 4; + } else if (m >= 7 && m <= 9) { + m2 = 7; + } + return getBeginTimeOfDayWithShort(dateTime.withMonthOfYear(m2).dayOfMonth().withMinimumValue()); + } + + /** + * 获取参数时间对象所在季度的结束时间的字符串短格式, + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfQuarter(DateTime dateTime) { + int m = dateTime.getMonthOfYear(); + int m2 = 12; + if (m >= 1 && m <= 3) { + m2 = 3; + } else if (m >= 4 && m <= 6) { + m2 = 6; + } else if (m >= 7 && m <= 9) { + m2 = 9; + } + return getEndTimeOfDayWithShort(dateTime.withMonthOfYear(m2).dayOfMonth().withMaximumValue()); + } + + /** + * 获取一天中的开始时间和结束时间的字符串格式,如2019-08-03 00:00:00.000 和 2019-08-03 23:59:59.999。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 包含格式后字符串的二元组对象。 + */ + public static Tuple2 getDateTimeRangeOfDay(DateTime dateTime) { + return new Tuple2<>(getBeginTimeOfDay(dateTime), getEndTimeOfDay(dateTime)); + } + + /** + * 获取本月第一天的日期格式。如2019-08-01。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateOfMonth(DateTime dateTime) { + return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT); + } + + /** + * 获取本月第一天的日期格式。如2019-08-01。 + * + * @param dateString 待格式化的日期字符串对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateOfMonth(String dateString) { + DateTime dateTime = toDate(dateString); + return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT); + } + + /** + * 计算指定日期距离今天相差的天数。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 相差天数。 + */ + public static int getDayDiffToNow(DateTime dateTime) { + return new Period(dateTime, new DateTime(), days()).getDays(); + } + + /** + * 将日期对象格式化为缺省的字符串格式。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String toDateString(DateTime dateTime) { + return dateTime.toString(COMMON_DATE_FORMAT); + } + + /** + * 将日期时间对象格式化为缺省的字符串格式。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String toDateTimeString(DateTime dateTime) { + return dateTime.toString(COMMON_DATETIME_FORMAT); + } + + /** + * 将缺省格式的日期字符串解析为日期对象。 + * + * @param dateString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDate(String dateString) { + return DATE_PARSE_FORMATTER.parseDateTime(dateString); + } + + /** + * 将缺省格式的日期字符串解析为日期对象。 + * + * @param dateTimeString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDateTime(String dateTimeString) { + return DATETIME_PARSE_FORMATTER.parseDateTime(dateTimeString); + } + + /** + * 将缺省格式的(不包含毫秒的)日期时间字符串解析为日期对象。 + * + * @param dateTimeString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDateTimeWithoutMs(String dateTimeString) { + return DATETIME_SHORT_PARSE_FORMATTER.parseDateTime(dateTimeString); + } + + /** + * 截取时间到天。如2019-10-03 01:20:30 转换为 2019-10-03 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToDay(Date date) { + return DateUtils.truncate(date, Calendar.DAY_OF_MONTH); + } + + /** + * 截取时间到月。如2019-10-03 01:20:30 转换为 2019-10-01 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToMonth(Date date) { + return DateUtils.truncate(date, Calendar.MONTH); + } + + /** + * 截取时间到年。如2019-10-03 01:20:30 转换为 2019-01-01 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToYear(Date date) { + return DateUtils.truncate(date, Calendar.YEAR); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyDateUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java new file mode 100644 index 00000000..70fca458 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyModelUtil.java @@ -0,0 +1,873 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.exception.InvalidDataFieldException; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreInfo; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 负责Model数据操作、类型转换和关系关联等行为的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MyModelUtil { + + /** + * 数值型字段。 + */ + public static final Integer NUMERIC_FIELD_TYPE = 0; + /** + * 字符型字段。 + */ + public static final Integer STRING_FIELD_TYPE = 1; + /** + * 日期型字段。 + */ + public static final Integer DATE_FIELD_TYPE = 2; + /** + * 整个工程的实体对象中,创建者Id字段的Java对象名。 + */ + public static final String CREATE_USER_ID_FIELD_NAME = "createUserId"; + /** + * 整个工程的实体对象中,创建时间字段的Java对象名。 + */ + public static final String CREATE_TIME_FIELD_NAME = "createTime"; + /** + * 整个工程的实体对象中,更新者Id字段的Java对象名。 + */ + public static final String UPDATE_USER_ID_FIELD_NAME = "updateUserId"; + /** + * 整个工程的实体对象中,更新时间字段的Java对象名。 + */ + public static final String UPDATE_TIME_FIELD_NAME = "updateTime"; + /** + * mapToColumnName和mapToColumnInfo使用的缓存。 + */ + private static final Map> CACHED_COLUMNINFO_MAP = new ConcurrentHashMap<>(); + + /** + * 将Bean转换为Map。 + * + * @param data Bean数据对象。 + * @param Bean对象类型。 + * @return 转换后的Map。 + */ + public static Map beanToMap(T data) { + return BeanUtil.beanToMap(data); + } + + /** + * 将Bean的数据列表转换为Map列表。 + * + * @param dataList Bean数据列表。 + * @param Bean对象类型。 + * @return 转换后的Map列表。 + */ + public static List> beanToMapList(List dataList) { + return CollUtil.isEmpty(dataList) ? new LinkedList<>() + : dataList.stream().map(BeanUtil::beanToMap).collect(Collectors.toList()); + } + + /** + * 将Map的数据列表转换为Bean列表。 + * + * @param dataList Map数据列表。 + * @param Bean对象类型。 + * @return 转换后的Bean对象列表。 + */ + public static List mapToBeanList(List> dataList, Class clazz) { + return CollUtil.isEmpty(dataList) ? new LinkedList<>() + : dataList.stream().map(data -> BeanUtil.toBeanIgnoreError(data, clazz)).collect(Collectors.toList()); + } + + /** + * 拷贝源类型的集合数据到目标类型的集合中,其中源类型和目标类型中的对象字段类型完全相同。 + * NOTE: 该函数主要应用于框架中,Dto和Model之间的copy,特别针对一对一关联的深度copy。 + * 在Dto中,一对一对象可以使用Map来表示,而不需要使用从表对象的Dto。 + * + * @param sourceCollection 源类型集合。 + * @param targetClazz 目标类型的Class对象。 + * @param 源类型。 + * @param 目标类型。 + * @return copy后的目标类型对象集合。 + */ + public static List copyCollectionTo(Collection sourceCollection, Class targetClazz) { + List targetList = null; + if (sourceCollection == null) { + return targetList; + } + targetList = new LinkedList<>(); + if (CollUtil.isNotEmpty(sourceCollection)) { + for (S source : sourceCollection) { + try { + T target = targetClazz.newInstance(); + BeanUtil.copyProperties(source, target); + targetList.add(target); + } catch (Exception e) { + log.error("Failed to call MyModelUtil.copyCollectionTo", e); + return Collections.emptyList(); + } + } + } + return targetList; + } + + /** + * 拷贝源类型的对象数据到目标类型的对象中,其中源类型和目标类型中的对象字段类型完全相同。 + * NOTE: 该函数主要应用于框架中,Dto和Model之间的copy,特别针对一对一关联的深度copy。 + * 在Dto中,一对一对象可以使用Map来表示,而不需要使用从表对象的Dto。 + * + * @param source 源类型对象。 + * @param targetClazz 目标类型的Class对象。 + * @param 源类型。 + * @param 目标类型。 + * @return copy后的目标类型对象。 + */ + public static T copyTo(S source, Class targetClazz) { + if (source == null) { + return null; + } + try { + T target = targetClazz.newInstance(); + BeanUtil.copyProperties(source, target); + return target; + } catch (Exception e) { + log.error("Failed to call MyModelUtil.copyTo", e); + return null; + } + } + + /** + * 映射Model对象的字段反射对象,获取与该字段对应的数据库列名称。 + * + * @param field 字段反射对象。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String mapToColumnName(Field field, Class modelClazz) { + return mapToColumnName(field.getName(), modelClazz); + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String mapToColumnName(String fieldName, Class modelClazz) { + Tuple2 columnInfo = mapToColumnInfo(fieldName, modelClazz); + return columnInfo == null ? null : columnInfo.getFirst(); + } + + /** + * 映射Model对象的字段反射对象,获取与该字段对应的数据库列名称。 + * 如果没有匹配到ColumnName,则立刻抛出异常。 + * + * @param field 字段反射对象。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String safeMapToColumnName(Field field, Class modelClazz) { + return safeMapToColumnName(field.getName(), modelClazz); + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称。 + * 如果没有匹配到ColumnName,则立刻抛出异常。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String safeMapToColumnName(String fieldName, Class modelClazz) { + String columnName = mapToColumnName(fieldName, modelClazz); + if (columnName == null) { + throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName); + } + return columnName; + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称和字段类型。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称和Java字段类型。 + */ + public static Tuple2 mapToColumnInfo(String fieldName, Class modelClazz) { + if (StrUtil.isBlank(fieldName)) { + return null; + } + StringBuilder sb = new StringBuilder(128); + sb.append(modelClazz.getName()).append("-#-").append(fieldName); + Tuple2 columnInfo = CACHED_COLUMNINFO_MAP.get(sb.toString()); + if (columnInfo != null) { + return columnInfo; + } + Field field = ReflectUtil.getField(modelClazz, fieldName); + if (field == null) { + return null; + } + TableField c = field.getAnnotation(TableField.class); + String columnName = null; + if (c == null) { + TableId id = field.getAnnotation(TableId.class); + if (id != null) { + columnName = id.value(); + } + } + if (StrUtil.isBlank(columnName)) { + columnName = c == null ? CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName) : c.value(); + if (StrUtil.isBlank(columnName)) { + columnName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName); + } + } + // 这里缺省情况下都是按照整型去处理,因为他覆盖太多的类型了。 + // 如Integer/Long/Double/BigDecimal,可根据实际情况完善和扩充。 + String typeName = field.getType().getSimpleName(); + Integer type = NUMERIC_FIELD_TYPE; + if (String.class.getSimpleName().equals(typeName)) { + type = STRING_FIELD_TYPE; + } else if (Date.class.getSimpleName().equals(typeName)) { + type = DATE_FIELD_TYPE; + } + columnInfo = new Tuple2<>(columnName, type); + CACHED_COLUMNINFO_MAP.put(sb.toString(), columnInfo); + return columnInfo; + } + + /** + * 映射Model主对象的Class名称,到Model所对应的表名称。 + * + * @param modelClazz Model主对象的Class。 + * @return Model对象对应的数据表名称。 + */ + public static String mapToTableName(Class modelClazz) { + TableName t = modelClazz.getAnnotation(TableName.class); + return t == null ? null : t.value(); + } + + /** + * 主Model类型中,遍历所有包含RelationConstDict注解的字段,并将关联的静态字典中的数据, + * 填充到thisModel对象的被注解字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModel 主对象。 + * @param 主表对象类型。 + */ + @SuppressWarnings("unchecked") + public static void makeConstDictRelation(Class thisClazz, T thisModel) { + if (thisModel == null) { + return; + } + Field[] fields = ReflectUtil.getFields(thisClazz); + for (Field field : fields) { + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName()); + RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class); + if (r == null) { + continue; + } + Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = + (Map) ReflectUtil.getFieldValue(r.constantDictClass(), dictMapField); + Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField()); + if (id != null) { + String name = dictMap.get(id); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + } + + /** + * 主Model类型中,遍历所有包含RelationConstDict注解的字段,并将关联的静态字典中的数据, + * 填充到thisModelList集合元素对象的被注解字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param 主表对象类型。 + */ + @SuppressWarnings("unchecked") + public static void makeConstDictRelation(Class thisClazz, List thisModelList) { + if (CollUtil.isEmpty(thisModelList)) { + return; + } + List thisModelList2 = thisModelList.stream().filter(Objects::nonNull).collect(Collectors.toList()); + Field[] fields = ReflectUtil.getFields(thisClazz); + for (Field field : fields) { + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName()); + RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class); + if (r == null) { + continue; + } + Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = + (Map) ReflectUtil.getFieldValue(r.constantDictClass(), dictMapField); + for (T thisModel : thisModelList2) { + Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField()); + if (id != null) { + String name = dictMap.get(id); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + } + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象thatModel中的数据, + * 关联到thisModel对象的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModel 主对象。 + * @param thatModel 字典关联对象。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, T thisModel, R thatModel, String thisRelationField) { + if (thatModel == null || thisModel == null) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Object slaveId = ReflectUtil.getFieldValue(thatModel, r.slaveIdField()); + if (slaveId != null) { + Map m = new HashMap<>(2); + m.put("id", slaveId); + m.put("name", ReflectUtil.getFieldValue(thatModel, r.slaveNameField())); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象集合thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 字典关联对象列表集合。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField()); + Map thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.put(id, thatModel); + } + }); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMap.get(id); + if (thatModel != null) { + Map m = new HashMap<>(4); + m.put("id", id); + m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField)); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象集合thatModelMap中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * 该函数之所以使用Map,主要出于性能优化考虑,在连续使用thatModelMap进行关联时,有效的避免了从多次从List转换到Map的过程。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatMadelMap 字典关联对象映射集合。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, List thisModelList, Map thatMadelMap, String thisRelationField) { + if (MapUtil.isEmpty(thatMadelMap) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField()); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMadelMap.get(id); + if (thatModel != null) { + Map m = new HashMap<>(4); + m.put("id", id); + m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField)); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationGlobalDict注解参数,全局字典dictMap中的字典数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param dictMap 全局字典数据。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + */ + public static void makeGlobalDictRelation( + Class thisClazz, List thisModelList, Map dictMap, String thisRelationField) { + if (MapUtil.isEmpty(dictMap) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationGlobalDict r = thisTargetField.getAnnotation(RelationGlobalDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + String name = dictMap.get(id.toString()); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationOneToOne注解参数,将被关联对象列表thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 一对一关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationOneToOne r = thisTargetField.getAnnotation(RelationOneToOne.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Map thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.put(id, thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMap.get(id); + if (thatModel != null) { + if (thisTargetField.getType().equals(Map.class)) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, BeanUtil.beanToMap(thatModel)); + } else { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + } + }); + } + + /** + * 根据主对象和关联对象各自的关联Id函数,将主对象列表和关联对象列表中的数据关联到一起,并将关联对象 + * 设置到主对象的指定关联字段中。 + * NOTE: 用于主对象关联字段中,没有包含RelationOneToOne注解的场景。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thisIdGetterFunc 主对象Id的Getter函数。 + * @param thatModelList 关联对象列表。 + * @param thatIdGetterFunc 关联对象Id的Getter函数。 + * @param thisRelationField 主对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, + List thisModelList, + Function thisIdGetterFunc, + List thatModelList, + Function thatIdGetterFunc, + String thisRelationField) { + makeOneToOneRelation(thisClazz, thisModelList, + thisIdGetterFunc, thatModelList, thatIdGetterFunc, thisRelationField, false); + } + + /** + * 根据主对象和关联对象各自的关联Id函数,将主对象列表和关联对象列表中的数据关联到一起,并将关联对象 + * 设置到主对象的指定关联字段中。 + * NOTE: 用于主对象关联字段中,没有包含RelationOneToOne注解的场景。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thisIdGetterFunc 主对象Id的Getter函数。 + * @param thatModelList 关联对象列表。 + * @param thatIdGetterFunc 关联对象Id的Getter函数。 + * @param thisRelationField 主对象中保存被关联对象的字段名称。 + * @param orderByThatList 如果为true,则按照ThatModelList的顺序输出。同时thisModelList被排序后的新列表替换。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, + List thisModelList, + Function thisIdGetterFunc, + List thatModelList, + Function thatIdGetterFunc, + String thisRelationField, + boolean orderByThatList) { + if (CollUtil.isEmpty(thisModelList)) { + return; + } + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + boolean isMap = thisTargetField.getType().equals(Map.class); + if (orderByThatList) { + List newThisModelList = new LinkedList<>(); + Map thisModelMap = + thisModelList.stream().collect(Collectors.toMap(thisIdGetterFunc, c -> c)); + thatModelList.forEach(thatModel -> { + Object thatId = thatIdGetterFunc.apply(thatModel); + if (thatId != null) { + T thisModel = thisModelMap.get(thatId); + if (thisModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel)); + newThisModelList.add(thisModel); + } + } + }); + thisModelList.clear(); + thisModelList.addAll(newThisModelList); + return; + } + Map thatMadelMap = + thatModelList.stream().collect(Collectors.toMap(thatIdGetterFunc, c -> c)); + thisModelList.forEach(thisModel -> { + Object thisId = thisIdGetterFunc.apply(thisModel); + if (thisId != null) { + R thatModel = thatMadelMap.get(thisId); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel)); + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationOneToMany注解参数,将被关联对象列表thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 一对多关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToManyRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationOneToMany r = thisTargetField.getAnnotation(RelationOneToMany.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Map> thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + List thatModelSubList = thatMap.computeIfAbsent(id, k -> new LinkedList<>()); + thatModelSubList.add(thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + List thatModel = thatMap.get(id); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationManyToMany注解参数,将被关联对象列表relationModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param idFieldName 主表主键Id字段名。 + * @param thisModelList 主对象列表。 + * @param relationModelList 多对多关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 关联表对象类型。 + */ + public static void makeManyToManyRelation( + Class thisClazz, String idFieldName, List thisModelList, List relationModelList, String thisRelationField) { + if (CollUtil.isEmpty(relationModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationManyToMany r = thisTargetField.getAnnotation(RelationManyToMany.class); + Field masterIdField = ReflectUtil.getField(thisClazz, idFieldName); + Class thatClass = r.relationModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.relationMasterIdField()); + Map> thatMap = new HashMap<>(20); + relationModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.computeIfAbsent(id, k -> new LinkedList<>()).add(thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + List thatModel = thatMap.get(id); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + }); + } + + private static Object normalize(boolean isMap, M model) { + return isMap ? BeanUtil.beanToMap(model) : model; + } + + /** + * 获取上传字段的存储信息。 + * + * @param modelClass model的class对象。 + * @param uploadFieldName 上传字段名。 + * @param model的类型。 + * @return 字段的上传存储信息对象。该值始终不会返回null。 + */ + public static UploadStoreInfo getUploadStoreInfo(Class modelClass, String uploadFieldName) { + UploadStoreInfo uploadStoreInfo = new UploadStoreInfo(); + Field uploadField = ReflectUtil.getField(modelClass, uploadFieldName); + if (uploadField == null) { + throw new UnsupportedOperationException("The Field [" + + uploadFieldName + "] doesn't exist in Model [" + modelClass.getSimpleName() + "]."); + } + uploadStoreInfo.setSupportUpload(false); + UploadFlagColumn anno = uploadField.getAnnotation(UploadFlagColumn.class); + if (anno != null) { + uploadStoreInfo.setSupportUpload(true); + uploadStoreInfo.setStoreType(anno.storeType()); + } + return uploadStoreInfo; + } + + /** + * 在插入实体对象数据之前,可以调用该方法,初始化通用字段的数据。 + * + * @param data 实体对象。 + * @param 实体对象类型。 + */ + public static void fillCommonsForInsert(M data) { + Field createdByField = ReflectUtil.getField(data.getClass(), CREATE_USER_ID_FIELD_NAME); + if (createdByField != null) { + ReflectUtil.setFieldValue(data, createdByField, TokenData.takeFromRequest().getUserId()); + } + Field createTimeField = ReflectUtil.getField(data.getClass(), CREATE_TIME_FIELD_NAME); + if (createTimeField != null) { + ReflectUtil.setFieldValue(data, createTimeField, new Date()); + } + Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME); + if (updatedByField != null) { + ReflectUtil.setFieldValue(data, updatedByField, TokenData.takeFromRequest().getUserId()); + } + Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME); + if (updateTimeField != null) { + ReflectUtil.setFieldValue(data, updateTimeField, new Date()); + } + } + + /** + * 在更新实体对象数据之前,可以调用该方法,更新通用字段的数据。 + * + * @param data 实体对象。 + * @param originalData 原有实体对象。 + * @param 实体对象类型。 + */ + public static void fillCommonsForUpdate(M data, M originalData) { + Object createdByValue = ReflectUtil.getFieldValue(originalData, CREATE_USER_ID_FIELD_NAME); + if (createdByValue != null) { + ReflectUtil.setFieldValue(data, CREATE_USER_ID_FIELD_NAME, createdByValue); + } + Object createTimeValue = ReflectUtil.getFieldValue(originalData, CREATE_TIME_FIELD_NAME); + if (createTimeValue != null) { + ReflectUtil.setFieldValue(data, CREATE_TIME_FIELD_NAME, createTimeValue); + } + Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME); + if (updatedByField != null) { + ReflectUtil.setFieldValue(data, updatedByField, TokenData.takeFromRequest().getUserId()); + } + Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME); + if (updateTimeField != null) { + ReflectUtil.setFieldValue(data, updateTimeField, new Date()); + } + } + + /** + * 为实体对象字段设置缺省值。如果data对象中指定字段的值为NULL,则设置缺省值,否则跳过。 + * + * @param data 实体对象。 + * @param fieldName 实体对象字段名。 + * @param defaultValue 缺省值。 + * @param 实体对象类型。 + * @param 缺省值类型。 + */ + public static void setDefaultValue(M data, String fieldName, V defaultValue) { + Object v = ReflectUtil.getFieldValue(data, fieldName); + if (v == null) { + ReflectUtil.setFieldValue(data, fieldName, defaultValue); + } + } + + /** + * 获取当前数据对象中,所有上传文件字段的数据,并将上传后的文件名存到集合中并返回。 + * + * @param data 数据对象。 + * @param clazz 数据对象的Class类型。 + * @param 数据对象类型。 + * @return 当前数据对象中,所有上传文件字段中,文件名属性的集合。 + */ + public static Set extractDownloadFileName(M data, Class clazz) { + Set resultSet = new HashSet<>(); + if (data == null) { + return resultSet; + } + Field[] fields = ReflectUtil.getFields(clazz); + for (Field field : fields) { + if (field.isAnnotationPresent(UploadFlagColumn.class)) { + String v = (String) ReflectUtil.getFieldValue(data, field); + List fileInfoList = JSON.parseArray(v, UploadResponseInfo.class); + if (CollUtil.isNotEmpty(fileInfoList)) { + fileInfoList.forEach(fileInfo -> resultSet.add(fileInfo.getFilename())); + } + } + } + return resultSet; + } + + /** + * 获取当前数据对象列表中,所有上传文件字段的数据,并将上传后的文件名存到集合中并返回。 + * + * @param dataList 数据对象。 + * @param clazz 数据对象的Class类型。 + * @param 数据对象类型。 + * @return 当前数据对象中,所有上传文件字段中,文件名属性的集合。 + */ + public static Set extractDownloadFileName(List dataList, Class clazz) { + Set resultSet = new HashSet<>(); + if (CollUtil.isEmpty(dataList)) { + return resultSet; + } + dataList.forEach(data -> resultSet.addAll(extractDownloadFileName(data, clazz))); + return resultSet; + } + + /** + * 根据数据对象指定字段的类型,将参数中的字段值集合转换为匹配的值类型集合。 + * @param clazz 数据对象的Class。 + * @param fieldName 字段名。 + * @param fieldValues 字符型的字段值集合。 + * @param 对象类型。 + * @return 转换后的字段值集合。 + */ + public static Set convertToTypeValues( + Class clazz, String fieldName, List fieldValues) { + Field f = ReflectUtil.getField(clazz, fieldName); + if (f == null) { + String errorMsg = "数据对象 [" + clazz.getSimpleName() + " ] 中,不存在该数据字段 [" + fieldName + "]!"; + throw new MyRuntimeException(errorMsg); + } + if (f.getType().equals(Long.class)) { + return fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet()); + } else if (f.getType().equals(Integer.class)) { + return fieldValues.stream().map(Integer::valueOf).collect(Collectors.toSet()); + } + return new HashSet<>(fieldValues); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyModelUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java new file mode 100644 index 00000000..fc2c7d8f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/MyPageUtil.java @@ -0,0 +1,155 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.jimmyshi.beanquery.BeanQuery; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.Page; +import org.apache.commons.collections4.CollectionUtils; +import com.orangeforms.common.core.base.mapper.BaseModelMapper; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.Tuple2; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 生成带有分页信息的数据列表 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MyPageUtil { + + private static final String DATA_LIST_LITERAL = "dataList"; + private static final String TOTAL_COUNT_LITERAL = "totalCount"; + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param includeFields 结果集中需要返回到前端的字段,多个字段之间逗号分隔。 + * @return 返回只是包含includeFields字段的数据列表,以及结果集TotalCount。 + */ + public static JSONObject makeResponseData(List dataList, String includeFields) { + JSONObject pageData = new JSONObject(); + pageData.put(DATA_LIST_LITERAL, BeanQuery.select(includeFields).from(dataList).execute()); + if (dataList instanceof Page) { + pageData.put(TOTAL_COUNT_LITERAL, ((Page)dataList).getTotal()); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList) { + MyPageData pageData = new MyPageData<>(); + pageData.setDataList(dataList); + if (dataList instanceof Page) { + pageData.setTotalCount(((Page)dataList).getTotal()); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param totalCount 总数量。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Long totalCount) { + MyPageData pageData = new MyPageData<>(); + pageData.setDataList(dataList); + if (totalCount != null) { + pageData.setTotalCount(totalCount); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param modelMapper 实体对象到DomainVO对象的数据映射器。 + * @param DomainVO对象类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, BaseModelMapper modelMapper) { + long totalCount = 0L; + if (CollectionUtils.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + return MyPageUtil.makeResponseData(modelMapper.fromModelList(dataList), totalCount); + } + + /** + * 构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param converter 转换函数对象。 + * @param 结果类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Function converter) { + long totalCount = 0L; + if (CollUtil.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + List resultList = dataList.stream().map(converter).collect(Collectors.toList()); + return MyPageUtil.makeResponseData(resultList, totalCount); + } + + /** + * 构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param targetClazz 模板对象类型。 + * @param 结果类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Class targetClazz) { + long totalCount = 0L; + if (CollUtil.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + List resultList = MyModelUtil.copyCollectionTo(dataList, targetClazz); + return MyPageUtil.makeResponseData(resultList, totalCount); + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param responseData 第一个数据时数据列表,第二个是列表数量。 + * @param 源数据类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(Tuple2, Long> responseData) { + return makeResponseData(responseData.getFirst(), responseData.getSecond()); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyPageUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java new file mode 100644 index 00000000..23494356 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RedisKeyUtil.java @@ -0,0 +1,187 @@ +package com.orangeforms.common.core.util; + +import com.orangeforms.common.core.object.TokenData; + +/** + * Redis 键生成工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class RedisKeyUtil { + + private static final String SESSIONID_PREFIX = "SESSIONID:"; + + /** + * 获取通用的session缓存的键前缀。 + * + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix() { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_"; + } + + /** + * 获取指定用户Id的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(String loginName) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_"; + } + + /** + * 获取指定用户Id的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @param tokenData 令牌对象。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(TokenData tokenData, String loginName) { + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_"; + } + + /** + * 获取指定用户Id和登录设备类型的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @param deviceType 设备类型。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(String loginName, int deviceType) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_" + deviceType + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_" + deviceType + "_"; + } + + /** + * 计算SessionId返回存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话存储于Redis中的键值。 + */ + public static String makeSessionIdKey(String sessionId) { + return SESSIONID_PREFIX + sessionId; + } + + /** + * 计算SessionId关联的权限数据存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的权限数据存储于Redis中的键值。 + */ + public static String makeSessionPermIdKey(String sessionId) { + return "PERM:" + sessionId; + } + + /** + * 计算SessionId关联的权限字存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的权限字存储于Redis中的键值。 + */ + public static String makeSessionPermCodeKey(String sessionId) { + return "PERM_CODE:" + sessionId; + } + + /** + * 计算SessionId关联的数据权限数据存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的数据权限数据存储于Redis中的键值。 + */ + public static String makeSessionDataPermIdKey(String sessionId) { + return "DATA_PERM:" + sessionId; + } + + /** + * 计算包含全局字典及其数据项的缓存键。 + * + * @param dictCode 全局字典编码。 + * @return 全局字典指定编码的缓存键。 + */ + public static String makeGlobalDictKey(String dictCode) { + return "GLOBAL_DICT:" + dictCode; + } + + /** + * 计算仅仅包含全局字典对象数据的缓存键。 + * + * @param dictCode 全局字典编码。 + * @return 全局字典指定编码的缓存键。 + */ + public static String makeGlobalDictOnlyKey(String dictCode) { + return "GLOBAL_DICT_ONLY:" + dictCode; + } + + /** + * 计算会话的菜单Id关联权限资源URL的缓存键。 + * + * @param sessionId 会话Id。 + * @param menuId 菜单Id。 + * @return 计算后的缓存键。 + */ + public static String makeSessionMenuPermKey(String sessionId, Object menuId) { + return "SESSION_MENU_ID:" + sessionId + "-" + menuId.toString(); + } + + /** + * 计算会话的菜单Id关联权限资源URL的缓存键的前缀。 + * + * @param sessionId 会话Id。 + * @return 计算后的缓存键前缀。 + */ + public static String getSessionMenuPermPrefix(String sessionId) { + return "SESSION_MENU_ID:" + sessionId + "-"; + } + + /** + * 计算会话关联的白名单URL的缓存键。 + * + * @param sessionId 会话Id。 + * @return 计算后的缓存键。 + */ + public static String makeSessionWhiteListPermKey(String sessionId) { + return "SESSION_WHITE_LIST:" + sessionId; + } + + /** + * 计算会话关联指定部门Ids的子部门Ids的缓存键。 + * + * @param sessionId 会话Id。 + * @param deptIds 部门Id,多个部门Id之间逗号分割。 + * @return 计算后的缓存键。 + */ + public static String makeSessionChildrenDeptIdKey(String sessionId, String deptIds) { + return "SESSION_CHILDREN_DEPT_ID:" + sessionId + "-" + deptIds; + } + + /** + * 计算租户编码的缓存键。 + * + * @param tenantCode 租户编码。 + */ + public static String makeTenantCodeKey(String tenantCode) { + return "TENANT_CODE:" + tenantCode; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RedisKeyUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java new file mode 100644 index 00000000..05d34fb9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/RsaUtil.java @@ -0,0 +1,102 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import lombok.extern.slf4j.Slf4j; + +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +import java.util.Map; + +/** + * Java RSA 加密工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class RsaUtil { + + /** + * 密钥长度 于原文长度对应 以及越长速度越慢 + */ + private static final int KEY_SIZE = 1024; + /** + * 用于封装随机产生的公钥与私钥 + */ + private static final Map KEY_MAP = MapUtil.newHashMap(); + + /** + * 随机生成密钥对。 + */ + public static void genKeyPair() throws NoSuchAlgorithmException { + // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + // 初始化密钥对生成器 + keyPairGen.initialize(KEY_SIZE, new SecureRandom()); + // 生成一个密钥对,保存在keyPair中 + KeyPair keyPair = keyPairGen.generateKeyPair(); + // 得到私钥 + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // 得到公钥 + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded()); + // 得到私钥字符串 + String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded()); + // 将公钥和私钥保存到Map + // 0表示公钥 + KEY_MAP.put(0, publicKeyString); + // 1表示私钥 + KEY_MAP.put(1, privateKeyString); + } + + /** + * RSA公钥加密。 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + */ + public static String encrypt(String str, String publicKey) { + RSA rsa = new RSA(null, publicKey); + return Base64.getEncoder().encodeToString(rsa.encrypt(str, KeyType.PublicKey)); + } + + /** + * RSA私钥解密。 + * + * @param str 加密字符串 + * @param privateKey 私钥 + * @return 明文 + */ + public static String decrypt(String str, String privateKey) { + RSA rsa = new RSA(privateKey, null); + // 64位解码加密后的字符串 + return new String(rsa.decrypt(Base64.getDecoder().decode(str), KeyType.PrivateKey)); + } + + public static void main(String[] args) throws Exception { + long temp = System.currentTimeMillis(); + // 生成公钥和私钥 + genKeyPair(); + // 加密字符串 + log.info("公钥:" + KEY_MAP.get(0)); + log.info("私钥:" + KEY_MAP.get(1)); + log.info("生成密钥消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + log.info("生成后的公钥前端使用!"); + log.info("生成后的私钥后台使用!"); + String message = "RSA测试ABCD~!@#$"; + log.info("原文:" + message); + temp = System.currentTimeMillis(); + String messageEn = encrypt(message, KEY_MAP.get(0)); + log.info("密文:" + messageEn); + log.info("加密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + temp = System.currentTimeMillis(); + String messageDe = decrypt(messageEn, KEY_MAP.get(1)); + log.info("解密:" + messageDe); + log.info("解密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java new file mode 100644 index 00000000..5931410e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/util/TreeNode.java @@ -0,0 +1,92 @@ +package com.orangeforms.common.core.util; + +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 将列表结构组建为树结构的工具类。 + * + * @param 对象类型。 + * @param 节点之间关联键的类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class TreeNode { + + private K id; + private K parentId; + private T data; + private List> childList = new ArrayList<>(); + + /** + * 将列表结构组建为树结构的工具方法。 + * + * @param dataList 数据列表结构。 + * @param idFunc 获取关联id的函数对象。 + * @param parentIdFunc 获取关联ParentId的函数对象。 + * @param root 根节点。 + * @param 数据对象类型。 + * @param 节点之间关联键的类型。 + * @return 源数据对象的树结构存储。 + */ + public static List> build( + List dataList, Function idFunc, Function parentIdFunc, K root) { + List> treeNodeList = new ArrayList<>(); + for (T data : dataList) { + if (ObjectUtil.equals(parentIdFunc.apply(data), idFunc.apply(data))) { + continue; + } + TreeNode dataNode = new TreeNode<>(); + dataNode.setId(idFunc.apply(data)); + dataNode.setParentId(parentIdFunc.apply(data)); + dataNode.setData(data); + treeNodeList.add(dataNode); + } + return root == null ? toBuildTreeWithoutRoot(treeNodeList) : toBuildTree(treeNodeList, root); + } + + private static List> toBuildTreeWithoutRoot(List> treeNodes) { + Map> treeNodeMap = + treeNodes.stream().collect(Collectors.toMap(TreeNode::getId, n -> n)); + List> treeNodeList = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + TreeNode parentNode = treeNodeMap.get(treeNode.getParentId()); + if (parentNode == null) { + treeNodeList.add(treeNode); + } else { + parentNode.add(treeNode); + } + } + return treeNodeList; + } + + private static List> toBuildTree(List> treeNodes, K root) { + List> treeNodeList = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + if (root.equals(treeNode.getParentId())) { + treeNodeList.add(treeNode); + } + for (TreeNode it : treeNodes) { + if (it.getParentId() == treeNode.getId()) { + if (treeNode.getChildList() == null) { + treeNode.setChildList(new ArrayList<>()); + } + treeNode.add(it); + } + } + } + return treeNodeList; + } + + private void add(TreeNode node) { + childList.add(node); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java new file mode 100644 index 00000000..a287fd56 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/AddGroup.java @@ -0,0 +1,10 @@ +package com.orangeforms.common.core.validator; + +/** + * 数据增加的验证分组。通常用于数据新增场景。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface AddGroup { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java new file mode 100644 index 00000000..00e43b6a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictRef.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.core.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 定义在Model对象中,标注字段值引用自指定的常量字典,和ConstDictRefValidator对象配合完成数据验证。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ConstDictValidator.class) +public @interface ConstDictRef { + + /** + * 引用的常量字典对象,该对象必须包含isValid的静态方法。 + * + * @return 最大长度。 + */ + Class constDictClass(); + + /** + * 超过边界后的错误消息提示。 + * + * @return 错误提示。 + */ + String message() default "无效的字典引用值!"; + + /** + * 验证分组。 + * + * @return 验证分组。 + */ + Class[] groups() default {}; + + /** + * 载荷对象类型。 + * + * @return 载荷对象。 + */ + Class[] payload() default {}; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java new file mode 100644 index 00000000..ba58a2a7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/ConstDictValidator.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.core.validator; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.lang.reflect.Method; + +/** + * * 数据字段自定义验证,用于验证Model中关联的常量字典值的合法性。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class ConstDictValidator implements ConstraintValidator { + + private ConstDictRef constDictRef; + + @Override + public void initialize(ConstDictRef constDictRef) { + this.constDictRef = constDictRef; + } + + @Override + public boolean isValid(Object s, ConstraintValidatorContext constraintValidatorContext) { + if (ObjectUtil.isEmpty(s)) { + return true; + } + Method method = + ReflectUtil.getMethodByName(constDictRef.constDictClass(), "isValid"); + return ReflectUtil.invokeStatic(method, s); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java new file mode 100644 index 00000000..c5a983fb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLength.java @@ -0,0 +1,55 @@ +package com.orangeforms.common.core.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 定义在Model或Dto对象中,UTF-8编码的字符串字段长度的上限和下限,和TextLengthValidator对象配合完成数据验证。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = TextLengthValidator.class) +public @interface TextLength { + + /** + * 字符串字段的最小长度。 + * + * @return 最小长度。 + */ + int min() default 0; + + /** + * 字符串字段的最大长度。 + * + * @return 最大长度。 + */ + int max() default Integer.MAX_VALUE; + + /** + * 超过边界后的错误消息提示。 + * + * @return 错误提示。 + */ + String message() default "字段长度超过最大字节数!"; + + /** + * 验证分组。 + * + * @return 验证分组。 + */ + Class[] groups() default { }; + + /** + * 载荷对象类型。 + * + * @return 载荷对象。 + */ + Class[] payload() default { }; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java new file mode 100644 index 00000000..5433bc2b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/TextLengthValidator.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.core.validator; + +import org.apache.commons.lang3.CharUtils; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * 数据字段自定义验证,用于验证Model中UTF-8编码的字符串字段的最大长度和最小长度。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class TextLengthValidator implements ConstraintValidator { + + private TextLength textLength; + + @Override + public void initialize(TextLength textLength) { + this.textLength = textLength; + } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (s == null) { + return true; + } + int length = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (CharUtils.isAscii(c)) { + ++length; + } else { + length += 2; + } + } + return length >= textLength.min() && length <= textLength.max(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java new file mode 100644 index 00000000..1c196a79 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-core/src/main/java/com/orangeforms/common/core/validator/UpdateGroup.java @@ -0,0 +1,11 @@ +package com.orangeforms.common.core.validator; + +/** + * 数据修改的验证分组。通常用于数据更新的场景。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface UpdateGroup { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/pom.xml new file mode 100644 index 00000000..e791d2f7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-datafilter + 1.0.0 + common-datafilter + jar + + + + com.orangeforms + common-core + 1.0.0 + + + com.orangeforms + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java new file mode 100644 index 00000000..91ab688d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/aop/DisableDataFilterAspect.java @@ -0,0 +1,42 @@ +package com.orangeforms.common.datafilter.aop; + +import com.orangeforms.common.core.object.GlobalThreadLocal; +import lombok.extern.slf4j.Slf4j; +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.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 禁用Mybatis拦截器数据过滤的AOP处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DisableDataFilterAspect { + + /** + * 所有标记了DisableDataFilter注解的类和方法。 + */ + @Pointcut("@within(com.orangeforms.common.core.annotation.DisableDataFilter) " + + "|| @annotation(com.orangeforms.common.core.annotation.DisableDataFilter)") + public void disableDataFilterPointCut() { + // 空注释,避免sonar警告 + } + + @Around("disableDataFilterPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + boolean dataFilterEnabled = GlobalThreadLocal.setDataFilter(false); + try { + return point.proceed(); + } finally { + GlobalThreadLocal.setDataFilter(dataFilterEnabled); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java new file mode 100644 index 00000000..eefef7b5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.datafilter.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-datafilter模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({DataFilterProperties.class}) +public class DataFilterAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java new file mode 100644 index 00000000..f4019a9d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterProperties.java @@ -0,0 +1,50 @@ +package com.orangeforms.common.datafilter.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common-datafilter模块的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-datafilter") +public class DataFilterProperties { + + /** + * 是否启用租户过滤。 + */ + @Value("${common-datafilter.tenant.enabled:false}") + private Boolean enabledTenantFilter; + + /** + * 是否启动数据权限过滤。 + */ + @Value("${common-datafilter.dataperm.enabled:false}") + private Boolean enabledDataPermFilter; + + /** + * 部门关联表的表名前缀,如zz_。该值主要用在MybatisDataFilterInterceptor拦截器中, + * 用于拼接数据权限过滤的SQL语句。 + */ + @Value("${common-datafilter.dataperm.deptRelationTablePrefix:}") + private String deptRelationTablePrefix; + + /** + * 该值为true的时候,在进行数据权限过滤时,会加上表名,如:zz_sys_user.dept_id = xxx。 + * 为false时,过滤条件不加表名,只是使用字段名,如:dept_id = xxx。该值目前主要适用于 + * Oracle分页SQL使用了子查询的场景。此场景下,由于子查询使用了别名,再在数据权限过滤条件中 + * 加上原有表名时,SQL语法会报错。 + */ + @Value("${common-datafilter.dataperm.addTableNamePrefix:true}") + private Boolean addTableNamePrefix; + + /** + * 是否打开menuId和当前url的匹配关系的验证。 + */ + @Value("${common-datafilter.dataperm.enableMenuPermVerify:true}") + private Boolean enableMenuPermVerify; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java new file mode 100644 index 00000000..2ba79d45 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/config/DataFilterWebMvcConfigurer.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.datafilter.config; + +import com.orangeforms.common.datafilter.interceptor.DataFilterInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 添加数据过滤相关的拦截器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +public class DataFilterWebMvcConfigurer implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new DataFilterInterceptor()).addPathPatterns("/**"); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java new file mode 100644 index 00000000..a20b9083 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/DataFilterInterceptor.java @@ -0,0 +1,42 @@ +package com.orangeforms.common.datafilter.interceptor; + +import com.orangeforms.common.core.object.GlobalThreadLocal; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 主要用于初始化,通过Mybatis拦截器插件进行数据过滤的标记。 + * 在调用controller接口处理方法之前,必须强制将数据过滤标记设置为缺省值。 + * 这样可以避免使用当前线程在处理上一个请求时,未能正常清理的数据过滤标记值。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class DataFilterInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + // 每次进入Controller接口之前,均主动打开数据权限验证。 + // 可以避免该Servlet线程在处理之前的请求时异常退出,从而导致该状态数据没有被正常清除。 + GlobalThreadLocal.setDataFilter(true); + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + // 这里需要加注释,否则sonar不happy。 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + GlobalThreadLocal.clearDataFilter(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java new file mode 100644 index 00000000..4e2253a0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/interceptor/MybatisDataFilterInterceptor.java @@ -0,0 +1,637 @@ +package com.orangeforms.common.datafilter.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.TableName; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.exception.NoDataPermException; +import com.orangeforms.common.core.object.GlobalThreadLocal; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.core.constant.DataPermRuleType; +import com.orangeforms.common.datafilter.config.DataFilterProperties; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SubSelect; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.statement.RoutingStatementHandler; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.plugin.*; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.sql.Connection; +import java.util.*; + +/** + * Mybatis拦截器。目前用于数据权限的统一拦截和注入处理。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) +@Slf4j +@Component +public class MybatisDataFilterInterceptor implements Interceptor { + + @Autowired + private RedissonClient redissonClient; + @Autowired + private DataFilterProperties properties; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + /** + * 对象缓存。由于Set是排序后的,因此在查找排除方法名称时效率更高。 + * 在应用服务启动的监听器中(LoadDataPermMapperListener),会调用当前对象的(loadMappersWithDataPerm)方法,加载缓存。 + */ + private final Map cachedDataPermMap = MapUtil.newHashMap(); + /** + * 租户租户对象缓存。 + */ + private final Map cachedTenantMap = MapUtil.newHashMap(); + + /** + * 预先加载与数据过滤相关的数据到缓存,该函数会在(LoadDataFilterInfoListener)监听器中调用。 + */ + @SuppressWarnings("all") + public void loadInfoWithDataFilter() { + Map mapperMap = + ApplicationContextHolder.getApplicationContext().getBeansOfType(BaseDaoMapper.class); + for (BaseDaoMapper mapperProxy : mapperMap.values()) { + // 优先处理jdk的代理 + Object proxy = ReflectUtil.getFieldValue(mapperProxy, "h"); + // 如果不是jdk的代理,再看看cjlib的代理。 + if (proxy == null) { + proxy = ReflectUtil.getFieldValue(mapperProxy, "CGLIB$CALLBACK_0"); + } + Class mapperClass = (Class) ReflectUtil.getFieldValue(proxy, "mapperInterface"); + if (mapperClass == null) { + try { + mapperProxy = (BaseDaoMapper) + ((Advised) ReflectUtil.getFieldValue(proxy, "advised")).getTargetSource().getTarget(); + proxy = ReflectUtil.getFieldValue(mapperProxy, "h"); + mapperClass = (Class) ReflectUtil.getFieldValue(proxy, "mapperInterface"); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + if (BooleanUtil.isTrue(properties.getEnabledTenantFilter())) { + loadTenantFilterData(mapperClass); + } + if (BooleanUtil.isTrue(properties.getEnabledDataPermFilter())) { + EnableDataPerm rule = mapperClass.getAnnotation(EnableDataPerm.class); + if (rule != null) { + loadDataPermFilterRules(mapperClass, rule); + } + } + } + } + + private void loadTenantFilterData(Class mapperClass) { + Class modelClass = (Class) ((ParameterizedType) + mapperClass.getGenericInterfaces()[0]).getActualTypeArguments()[0]; + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field field : fields) { + if (field.getAnnotation(TenantFilterColumn.class) != null) { + ModelTenantInfo tenantInfo = new ModelTenantInfo(); + tenantInfo.setModelName(modelClass.getSimpleName()); + tenantInfo.setTableName(modelClass.getAnnotation(TableName.class).value()); + tenantInfo.setFieldName(field.getName()); + tenantInfo.setColumnName(MyModelUtil.mapToColumnName(field, modelClass)); + // 判断当前dao中是否包括不需要自动注入租户Id过滤的方法。 + DisableTenantFilter disableTenantFilter = mapperClass.getAnnotation(DisableTenantFilter.class); + if (disableTenantFilter != null) { + // 这里开始获取当前Mapper已经声明的的SqlId中,有哪些是需要排除在外的。 + // 排除在外的将不进行数据过滤。 + Set excludeMethodNameSet = new HashSet<>(); + for (String excludeName : disableTenantFilter.includeMethodName()) { + excludeMethodNameSet.add(excludeName); + // 这里是给pagehelper中,分页查询先获取数据总量的查询。 + excludeMethodNameSet.add(excludeName + "_COUNT"); + } + tenantInfo.setExcludeMethodNameSet(excludeMethodNameSet); + } + cachedTenantMap.put(mapperClass.getName(), tenantInfo); + break; + } + } + } + + private void loadDataPermFilterRules(Class mapperClass, EnableDataPerm rule) { + String sysDataPermMapperName = "SysDataPermMapper"; + // 由于给数据权限Mapper添加@EnableDataPerm,将会导致无限递归,因此这里检测到之后, + // 会在系统启动加载监听器的时候,及时抛出异常。 + if (StrUtil.equals(sysDataPermMapperName, mapperClass.getSimpleName())) { + throw new IllegalStateException("Add @EnableDataPerm annotation to SysDataPermMapper is ILLEGAL!"); + } + // 这里开始获取当前Mapper已经声明的的SqlId中,有哪些是需要排除在外的。 + // 排除在外的将不进行数据过滤。 + Set excludeMethodNameSet = null; + String[] excludes = rule.excluseMethodName(); + if (excludes.length > 0) { + excludeMethodNameSet = new HashSet<>(); + for (String excludeName : excludes) { + excludeMethodNameSet.add(excludeName); + // 这里是给pagehelper中,分页查询先获取数据总量的查询。 + excludeMethodNameSet.add(excludeName + "_COUNT"); + } + } + // 获取Mapper关联的主表信息,包括表名,user过滤字段名和dept过滤字段名。 + Class modelClazz = (Class) + ((ParameterizedType) mapperClass.getGenericInterfaces()[0]).getActualTypeArguments()[0]; + Field[] fields = ReflectUtil.getFields(modelClazz); + Field userFilterField = null; + Field deptFilterField = null; + for (Field field : fields) { + if (null != field.getAnnotation(UserFilterColumn.class)) { + userFilterField = field; + } + if (null != field.getAnnotation(DeptFilterColumn.class)) { + deptFilterField = field; + } + if (userFilterField != null && deptFilterField != null) { + break; + } + } + // 通过注解解析与Mapper关联的Model,并获取与数据权限关联的信息,并将结果缓存。 + ModelDataPermInfo info = new ModelDataPermInfo(); + info.setMainTableName(MyModelUtil.mapToTableName(modelClazz)); + info.setMustIncludeUserRule(rule.mustIncludeUserRule()); + info.setExcludeMethodNameSet(excludeMethodNameSet); + if (userFilterField != null) { + info.setUserFilterColumn(MyModelUtil.mapToColumnName(userFilterField, modelClazz)); + } + if (deptFilterField != null) { + info.setDeptFilterColumn(MyModelUtil.mapToColumnName(deptFilterField, modelClazz)); + } + cachedDataPermMap.put(mapperClass.getName(), info); + } + + @Override + public Object intercept(Invocation invocation) throws Throwable { + // 判断当前线程本地存储中,业务操作是否禁用了数据权限过滤,如果禁用,则不进行后续的数据过滤处理了。 + if (!GlobalThreadLocal.enabledDataFilter() + && BooleanUtil.isFalse(properties.getEnabledTenantFilter())) { + return invocation.proceed(); + } + // 只有在HttpServletRequest场景下,该拦截器才起作用,对于系统级别的预加载数据不会应用数据权限。 + if (!ContextUtil.hasRequestContext()) { + return invocation.proceed(); + } + // 没有登录的用户,不会参与租户过滤,如果需要过滤的,自己在代码中手动实现 + // 通常对于无需登录的白名单url,也无需过滤了。 + // 另外就是登录接口中,获取菜单列表的接口,由于尚未登录,没有TokenData,所以这个接口我们手动加入了该条件。 + if (TokenData.takeFromRequest() == null) { + return invocation.proceed(); + } + RoutingStatementHandler handler; + try { + handler = (RoutingStatementHandler) invocation.getTarget(); + } catch (Exception e) { + handler = (RoutingStatementHandler) + ReflectUtil.getFieldValue(ReflectUtil.getFieldValue(invocation.getTarget(), "h"), "target"); + } + StatementHandler delegate = + (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate"); + // 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性 + MappedStatement mappedStatement = + (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement"); + SqlCommandType commandType = mappedStatement.getSqlCommandType(); + // 对于INSERT语句,我们不进行任何数据过滤。 + if (commandType == SqlCommandType.INSERT) { + return invocation.proceed(); + } + String sqlId = mappedStatement.getId(); + int pos = StrUtil.lastIndexOfIgnoreCase(sqlId, "."); + String className = StrUtil.sub(sqlId, 0, pos); + String methodName = StrUtil.subSuf(sqlId, pos + 1); + // 先进行租户过滤条件的处理,再将解析并处理后的SQL Statement交给下一步的数据权限过滤去处理。 + // 这样做的目的主要是为了减少一次SQL解析的过程,因为这是高频操作,所以要尽量去优化。 + Statement statement = null; + if (BooleanUtil.isTrue(properties.getEnabledTenantFilter())) { + statement = this.processTenantFilter(className, methodName, delegate.getBoundSql(), commandType); + } + // 处理数据权限过滤。 + if (GlobalThreadLocal.enabledDataFilter() + && BooleanUtil.isTrue(properties.getEnabledDataPermFilter())) { + this.processDataPermFilter(className, methodName, delegate.getBoundSql(), commandType, statement, sqlId); + } + return invocation.proceed(); + } + + private Statement processTenantFilter( + String className, String methodName, BoundSql boundSql, SqlCommandType commandType) throws JSQLParserException { + ModelTenantInfo info = cachedTenantMap.get(className); + if (info == null || CollUtil.contains(info.getExcludeMethodNameSet(), methodName)) { + return null; + } + String sql = boundSql.getSql(); + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder filterBuilder = new StringBuilder(64); + filterBuilder.append(info.tableName).append(".") + .append(info.columnName) + .append("=") + .append(TokenData.takeFromRequest().getTenantId()); + String dataFilter = filterBuilder.toString(); + if (commandType == SqlCommandType.UPDATE) { + Update update = (Update) statement; + this.buildWhereClause(update, dataFilter); + } else if (commandType == SqlCommandType.DELETE) { + Delete delete = (Delete) statement; + this.buildWhereClause(delete, dataFilter); + } else { + Select select = (Select) statement; + PlainSelect selectBody = (PlainSelect) select.getSelectBody(); + FromItem fromItem = selectBody.getFromItem(); + if (fromItem != null) { + PlainSelect subSelect = null; + if (fromItem instanceof SubSelect) { + subSelect = (PlainSelect) ((SubSelect) fromItem).getSelectBody(); + } + if (subSelect != null) { + dataFilter = replaceTableAlias(info.getTableName(), subSelect, dataFilter); + buildWhereClause(subSelect, dataFilter); + } else { + dataFilter = replaceTableAlias(info.getTableName(), selectBody, dataFilter); + buildWhereClause(selectBody, dataFilter); + } + } + } + log.info("Tenant Filter Where Clause [{}]", dataFilter); + ReflectUtil.setFieldValue(boundSql, "sql", statement.toString()); + return statement; + } + + private void processDataPermFilter( + String className, String methodName, BoundSql boundSql, SqlCommandType commandType, Statement statement, String sqlId) + throws JSQLParserException { + // 判断当前线程本地存储中,业务操作是否禁用了数据权限过滤,如果禁用,则不进行后续的数据过滤处理了。 + // 数据过滤权限中,INSERT不过滤。如果是管理员则不参与数据权限的数据过滤,显示全部数据。 + TokenData tokenData = TokenData.takeFromRequest(); + if (Boolean.TRUE.equals(tokenData.getIsAdmin())) { + return; + } + ModelDataPermInfo info = cachedDataPermMap.get(className); + // 再次查找当前方法是否为排除方法,如果不是,就参与数据权限注入过滤。 + if (info == null || CollUtil.contains(info.getExcludeMethodNameSet(), methodName)) { + return; + } + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + Object cachedData = this.getCachedData(dataPermSessionKey); + if (cachedData == null) { + throw new NoDataPermException(StrFormatter.format( + "No Related DataPerm found for SQL_ID [{}] from Cache.", sqlId)); + } + JSONObject allMenuDataPermMap = cachedData instanceof JSONObject + ? (JSONObject) cachedData : JSON.parseObject(cachedData.toString()); + JSONObject menuDataPermMap = this.getAndVerifyMenuDataPerm(allMenuDataPermMap, sqlId); + Map dataPermMap = new HashMap<>(8); + for (Map.Entry entry : menuDataPermMap.entrySet()) { + dataPermMap.put(Integer.valueOf(entry.getKey()), entry.getValue().toString()); + } + if (MapUtil.isEmpty(dataPermMap)) { + throw new NoDataPermException(StrFormatter.format( + "No Related DataPerm found for SQL_ID [{}].", sqlId)); + } + if (dataPermMap.containsKey(DataPermRuleType.TYPE_ALL)) { + return; + } + // 如果当前过滤注解中mustIncludeUserRule参数为true,同时当前用户的数据权限中,不包含TYPE_USER_ONLY, + // 这里就需要自动添加该数据权限。 + if (info.getMustIncludeUserRule() + && !dataPermMap.containsKey(DataPermRuleType.TYPE_USER_ONLY)) { + dataPermMap.put(DataPermRuleType.TYPE_USER_ONLY, null); + } + this.processDataPerm(info, dataPermMap, boundSql, commandType, statement); + } + + private JSONObject getAndVerifyMenuDataPerm(JSONObject allMenuDataPermMap, String sqlId) { + String menuId = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_MENU_ID); + if (menuId == null) { + menuId = ContextUtil.getHttpRequest().getParameter(ApplicationConstant.HTTP_HEADER_MENU_ID); + } + if (BooleanUtil.isFalse(properties.getEnableMenuPermVerify()) && menuId == null) { + menuId = ApplicationConstant.DATA_PERM_ALL_MENU_ID; + } + Assert.notNull(menuId); + JSONObject menuDataPermMap = allMenuDataPermMap.getJSONObject(menuId); + if (menuDataPermMap == null) { + menuDataPermMap = allMenuDataPermMap.getJSONObject(ApplicationConstant.DATA_PERM_ALL_MENU_ID); + } + if (menuDataPermMap == null) { + throw new NoDataPermException(StrFormatter.format( + "No Related DataPerm found for menuId [{}] and SQL_ID [{}].", menuId, sqlId)); + } + if (BooleanUtil.isTrue(properties.getEnableMenuPermVerify())) { + String url = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_ORIGINAL_REQUEST_URL); + if (StrUtil.isBlank(url)) { + url = ContextUtil.getHttpRequest().getRequestURI(); + } + Assert.notNull(url); + if (!this.verifyMenuPerm(null, url, sqlId) && !this.verifyMenuPerm(menuId, url, sqlId)) { + String msg = StrFormatter.format("Mismatched DataPerm " + + "for menuId [{}] and url [{}] and SQL_ID [{}].", menuId, url, sqlId); + throw new NoDataPermException(msg); + } + } + return menuDataPermMap; + } + + private Object getCachedData(String dataPermSessionKey) { + Object cachedData; + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.DATA_PERMISSION_CACHE.name()); + org.springframework.util.Assert.notNull(cache, "Cache [DATA_PERMISSION_CACHE] can't be null."); + Cache.ValueWrapper wrapper = cache.get(dataPermSessionKey); + if (wrapper == null) { + cachedData = redissonClient.getBucket(dataPermSessionKey).get(); + if (cachedData != null) { + cache.put(dataPermSessionKey, JSON.parseObject(cachedData.toString())); + } + } else { + cachedData = wrapper.get(); + } + return cachedData; + } + + @SuppressWarnings("unchecked") + private boolean verifyMenuPerm(String menuId, String url, String sqlId) { + String sessionId = TokenData.takeFromRequest().getSessionId(); + String menuPermSessionKey; + if (menuId != null) { + menuPermSessionKey = RedisKeyUtil.makeSessionMenuPermKey(sessionId, menuId); + } else { + menuPermSessionKey = RedisKeyUtil.makeSessionWhiteListPermKey(sessionId); + } + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.MENU_PERM_CACHE.name()); + org.springframework.util.Assert.notNull(cache, "Cache [MENU_PERM_CACHE] can't be null!"); + Cache.ValueWrapper wrapper = cache.get(menuPermSessionKey); + if (wrapper != null) { + Object cachedData = wrapper.get(); + if (cachedData != null) { + return ((Set) cachedData).contains(url); + } + } + RBucket bucket = redissonClient.getBucket(menuPermSessionKey); + if (!bucket.isExists()) { + String msg; + if (menuId == null) { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for WHITE_LIST and SQL_ID [{}] with sessionId [{}].", sqlId, sessionId); + } else { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for menuId [{}] and SQL_ID [{}] with sessionId [{}].", menuId, sqlId, sessionId); + } + throw new NoDataPermException(msg); + } + Set cachedMenuPermSet = new HashSet<>(JSONArray.parseArray(bucket.get(), String.class)); + cache.put(menuPermSessionKey, cachedMenuPermSet); + return cachedMenuPermSet.contains(url); + } + + private void processDataPerm( + ModelDataPermInfo info, + Map dataPermMap, + BoundSql boundSql, + SqlCommandType commandType, + Statement statement) throws JSQLParserException { + List criteriaList = new LinkedList<>(); + for (Map.Entry entry : dataPermMap.entrySet()) { + String filterClause = processDataPermRule(info, entry.getKey(), entry.getValue()); + if (StrUtil.isNotBlank(filterClause)) { + criteriaList.add(filterClause); + } + } + if (CollUtil.isEmpty(criteriaList)) { + return; + } + StringBuilder filterBuilder = new StringBuilder(128); + filterBuilder.append("("); + filterBuilder.append(StrUtil.join(" OR ", criteriaList)); + filterBuilder.append(")"); + String dataFilter = filterBuilder.toString(); + if (statement == null) { + String sql = boundSql.getSql(); + statement = CCJSqlParserUtil.parse(sql); + } + if (commandType == SqlCommandType.UPDATE) { + Update update = (Update) statement; + this.buildWhereClause(update, dataFilter); + } else if (commandType == SqlCommandType.DELETE) { + Delete delete = (Delete) statement; + this.buildWhereClause(delete, dataFilter); + } else { + this.processSelect(statement, info, dataFilter); + } + log.info("DataPerm Filter Where Clause [{}]", dataFilter); + ReflectUtil.setFieldValue(boundSql, "sql", statement.toString()); + } + + private void processSelect(Statement statement, ModelDataPermInfo info, String dataFilter) + throws JSQLParserException { + Select select = (Select) statement; + PlainSelect selectBody = (PlainSelect) select.getSelectBody(); + FromItem fromItem = selectBody.getFromItem(); + if (fromItem == null) { + return; + } + PlainSelect subSelect = null; + if (fromItem instanceof SubSelect) { + subSelect = (PlainSelect) ((SubSelect) fromItem).getSelectBody(); + } + if (subSelect != null) { + dataFilter = replaceTableAlias(info.getMainTableName(), subSelect, dataFilter); + buildWhereClause(subSelect, dataFilter); + } else { + dataFilter = replaceTableAlias(info.getMainTableName(), selectBody, dataFilter); + buildWhereClause(selectBody, dataFilter); + } + } + + private String processDataPermRule(ModelDataPermInfo info, Integer ruleType, String dataIds) { + TokenData tokenData = TokenData.takeFromRequest(); + StringBuilder filter = new StringBuilder(128); + String tableName = info.getMainTableName(); + if (ruleType != DataPermRuleType.TYPE_USER_ONLY + && ruleType != DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS + && ruleType != DataPermRuleType.TYPE_DEPT_USERS) { + return this.processDeptDataPermRule(info, ruleType, dataIds); + } + if (StrUtil.isBlank(info.getUserFilterColumn())) { + log.warn("No UserFilterColumn for table [{}] but USER_FILTER_DATA_PERM exists !!!", tableName); + return filter.toString(); + } + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + if (ruleType == DataPermRuleType.TYPE_USER_ONLY) { + filter.append(info.getUserFilterColumn()) + .append(" = ") + .append(tokenData.getUserId()); + } else { + filter.append(info.getUserFilterColumn()) + .append(" IN (") + .append(dataIds) + .append(") "); + } + return filter.toString(); + } + + private String processDeptDataPermRule(ModelDataPermInfo info, Integer ruleType, String deptIds) { + StringBuilder filter = new StringBuilder(128); + String tableName = info.getMainTableName(); + if (StrUtil.isBlank(info.getDeptFilterColumn())) { + log.warn("No DeptFilterColumn for table [{}] but DEPT_FILTER_DATA_PERM exists !!!", tableName); + return filter.toString(); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (ruleType == DataPermRuleType.TYPE_DEPT_ONLY) { + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" = ") + .append(tokenData.getDeptId()); + } else if (ruleType == DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id = ") + .append(tokenData.getDeptId()) + .append(" AND "); + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" = ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id IN (") + .append(deptIds) + .append(") AND "); + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" = ") + .append(properties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) { + if (BooleanUtil.isTrue(properties.getAddTableNamePrefix())) { + filter.append(info.getMainTableName()).append("."); + } + filter.append(info.getDeptFilterColumn()) + .append(" IN (") + .append(deptIds) + .append(") "); + } + return filter.toString(); + } + + private String replaceTableAlias(String tableName, PlainSelect select, String dataFilter) { + if (select.getFromItem().getAlias() == null) { + return dataFilter; + } + return dataFilter.replaceAll(tableName, select.getFromItem().getAlias().getName()); + } + + private void buildWhereClause(Update update, String dataFilter) throws JSQLParserException { + if (update.getWhere() == null) { + update.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter)); + } else { + AndExpression and = new AndExpression( + CCJSqlParserUtil.parseCondExpression(dataFilter), update.getWhere()); + update.setWhere(and); + } + } + + private void buildWhereClause(Delete delete, String dataFilter) throws JSQLParserException { + if (delete.getWhere() == null) { + delete.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter)); + } else { + AndExpression and = new AndExpression( + CCJSqlParserUtil.parseCondExpression(dataFilter), delete.getWhere()); + delete.setWhere(and); + } + } + + private void buildWhereClause(PlainSelect select, String dataFilter) throws JSQLParserException { + if (select.getWhere() == null) { + select.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter)); + } else { + AndExpression and = new AndExpression( + CCJSqlParserUtil.parseCondExpression(dataFilter), select.getWhere()); + select.setWhere(and); + } + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + // 这里需要空注解,否则sonar会不happy。 + } + + @Data + private static final class ModelDataPermInfo { + private Set excludeMethodNameSet; + private String userFilterColumn; + private String deptFilterColumn; + private String mainTableName; + private Boolean mustIncludeUserRule; + } + + @Data + private static final class ModelTenantInfo { + private Set excludeMethodNameSet; + private String modelName; + private String tableName; + private String fieldName; + private String columnName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java new file mode 100644 index 00000000..5d7cb78b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/java/com/orangeforms/common/datafilter/listener/LoadDataFilterInfoListener.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.datafilter.listener; + +import com.orangeforms.common.datafilter.interceptor.MybatisDataFilterInterceptor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +/** + * 应用服务启动监听器。 + * 目前主要功能是调用MybatisDataFilterInterceptor中的loadInfoWithDataFilter方法, + * 将标记有过滤注解的数据加载到缓存,以提升系统运行时效率。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class LoadDataFilterInfoListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + MybatisDataFilterInterceptor interceptor = + applicationReadyEvent.getApplicationContext().getBean(MybatisDataFilterInterceptor.class); + interceptor.loadInfoWithDataFilter(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..a08c930a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-datafilter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.datafilter.config.DataFilterAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/pom.xml new file mode 100644 index 00000000..e7ba325b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/pom.xml @@ -0,0 +1,54 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-dbutil + 1.0.0 + common-dbutil + jar + + + + com.orangeforms + common-core + 1.0.0 + + + mysql + mysql-connector-java + 8.0.22 + + + org.postgresql + postgresql + runtime + + + com.oracle.database.jdbc + ojdbc6 + 11.2.0.4 + + + com.dameng + DmJdbcDriver18 + 8.1.2.141 + + + org.opengauss + opengauss-jdbc + 5.0.0-og + + + ru.yandex.clickhouse + clickhouse-jdbc + 0.3.2 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java new file mode 100644 index 00000000..258b9a73 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/CustomDateValueType.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.dbutil.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 自定义日期过滤值类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class CustomDateValueType { + /** + * 本日。 + */ + public static final String CURRENT_DAY = "1"; + /** + * 本周。 + */ + public static final String CURRENT_WEEK = "2"; + /** + * 本月。 + */ + public static final String CURRENT_MONTH = "3"; + /** + * 本季度。 + */ + public static final String CURRENT_QUARTER = "4"; + /** + * 今年。 + */ + public static final String CURRENT_YEAR = "5"; + /** + * 昨天。 + */ + public static final String LAST_DAY = "11"; + /** + * 上周。 + */ + public static final String LAST_WEEK = "12"; + /** + * 上月。 + */ + public static final String LAST_MONTH = "13"; + /** + * 上季度。 + */ + public static final String LAST_QUARTER = "14"; + /** + * 去年。 + */ + public static final String LAST_YEAR = "15"; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(CURRENT_DAY, "本日"); + DICT_MAP.put(CURRENT_WEEK, "本周"); + DICT_MAP.put(CURRENT_MONTH, "本月"); + DICT_MAP.put(CURRENT_QUARTER, "本季度"); + DICT_MAP.put(CURRENT_YEAR, "今年"); + DICT_MAP.put(LAST_DAY, "昨日"); + DICT_MAP.put(LAST_WEEK, "上周"); + DICT_MAP.put(LAST_MONTH, "上月"); + DICT_MAP.put(LAST_QUARTER, "上季度"); + DICT_MAP.put(LAST_YEAR, "去年"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(String value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private CustomDateValueType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java new file mode 100644 index 00000000..83c2ecef --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/constant/DblinkType.java @@ -0,0 +1,74 @@ +package com.orangeforms.common.dbutil.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据库连接类型常量对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class DblinkType { + + /** + * MySQL。 + */ + public static final int MYSQL = 0; + /** + * PostgreSQL。 + */ + public static final int POSTGRESQL = 1; + /** + * Oracle。 + */ + public static final int ORACLE = 2; + /** + * Dameng。 + */ + public static final int DAMENG = 3; + /** + * 人大金仓。 + */ + public static final int KINGBASE = 4; + /** + * OpenGauss。 + */ + public static final int OPENGAUSS = 5; + /** + * ClickHouse。 + */ + public static final int CLICKHOUSE = 10; + /** + * Doris。 + */ + public static final int DORIS = 11; + + private static final Map DICT_MAP = new HashMap<>(3); + static { + DICT_MAP.put(MYSQL, "MySQL"); + DICT_MAP.put(POSTGRESQL, "PostgreSQL"); + DICT_MAP.put(ORACLE, "Oracle"); + DICT_MAP.put(DAMENG, "Dameng"); + DICT_MAP.put(KINGBASE, "人大金仓"); + DICT_MAP.put(OPENGAUSS, "OpenGauss"); + DICT_MAP.put(CLICKHOUSE, "ClickHouse"); + DICT_MAP.put(DORIS, "Doris"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DblinkType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java new file mode 100644 index 00000000..8ec9d20a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetFilter.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.dbutil.object; + +import com.orangeforms.common.core.constant.FieldFilterType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; + +/** + * 数据集过滤对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class DatasetFilter extends ArrayList { + + @Data + public static class FilterInfo { + /** + * 过滤的数据集Id。 + */ + private Long datasetId; + /** + * 过滤参数名称。 + */ + private String paramName; + /** + * 过滤参数值是单值时。使用该字段值。 + */ + private Object paramValue; + /** + * 过滤参数值是集合时,使用该字段值。 + */ + private Collection paramValueList; + /** + * 过滤类型。参考常量类 FieldFilterType。 + */ + private Integer filterType = FieldFilterType.EQUAL; + /** + * 是否为日期值的过滤。 + */ + private Boolean dateValueFilter = false; + /** + * 日期精确到。year/month/week/day + */ + private String dateRange; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java new file mode 100644 index 00000000..03886f41 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/DatasetParam.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.dbutil.object; + +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageParam; +import lombok.Data; + +import java.util.List; + +/** + * 数据集查询的各种参数。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class DatasetParam { + + /** + * SELECT选择的字段名列表。 + */ + private List selectColumnNameList; + /** + * 数据集过滤参数。 + */ + private DatasetFilter filter; + /** + * SQL结果集的参数。 + */ + private DatasetFilter sqlFilter; + /** + * 分页参数。 + */ + private MyPageParam pageParam; + /** + * 分组参数。 + */ + private MyOrderParam orderParam; + /** + * 排序字符串。 + */ + private String orderBy; + /** + * 该值目前仅用于SQL类型的结果集。 + * 如果该值为true,SQL结果集中定义的参数都会被替换为 (1 = 1) 的恒成立过滤。 + * 比如 select * from zz_sys_user where user_status = ${status}, + * 该值为true的时会被替换为 select * from zz_sys_user where 1 = 1。 + */ + private Boolean disableSqlDatasetFilter = false; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java new file mode 100644 index 00000000..f3151866 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/GenericResultSet.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.dbutil.object; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 报表通用的查询结果集对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class GenericResultSet { + + /** + * 查询结果集的字段meta数据列表。 + */ + private List columnMetaList; + + /** + * 查询数据集。如果当前结果集为分页查询,将只包含分页数据。 + */ + private List dataList; + + /** + * 查询数据总数。如果当前结果集为分页查询,该值为分页前的数据总数,否则为0。 + */ + private Long totalCount = 0L; + + public GenericResultSet(List columnMetaList, List dataList) { + this.columnMetaList = columnMetaList; + this.dataList = dataList; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java new file mode 100644 index 00000000..7c927194 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlResultSet.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.dbutil.object; + +import cn.hutool.core.collection.CollUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 直接从数据库获取的查询结果集对象。通常内部使用。 + * + * @author Jerry + * @date 2024-07-02 + */ +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Data +public class SqlResultSet extends GenericResultSet { + + public SqlResultSet(List columnMetaList, List dataList) { + super(columnMetaList, dataList); + } + + public static boolean isEmpty(SqlResultSet rs) { + return rs == null || CollUtil.isEmpty(rs.getDataList()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java new file mode 100644 index 00000000..fdda9cf8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTable.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.dbutil.object; + +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * 数据库中的表对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SqlTable { + + /** + * 表名称。 + */ + private String tableName; + + /** + * 表注释。 + */ + private String tableComment; + + /** + * 创建时间。 + */ + private Date createTime; + + /** + * 关联的字段列表。 + */ + private List columnList; + + /** + * 数据库链接Id。 + */ + private Long dblinkId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java new file mode 100644 index 00000000..afd5763f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/object/SqlTableColumn.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.dbutil.object; + +import lombok.Data; + +/** + * 数据库中的表字段对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class SqlTableColumn { + + /** + * 表字段名。 + */ + private String columnName; + + /** + * 字段注释。 + */ + private String columnComment; + + /** + * 表字段类型。 + */ + private String columnType; + + /** + * 表字段全类型。 + */ + private String fullColumnType; + + /** + * 是否自动增长。 + */ + private Boolean autoIncrement; + + /** + * 是否为主键。 + */ + private Boolean primaryKey; + + /** + * 是否可以为空值。 + */ + private Boolean nullable; + + /** + * 字段顺序。 + */ + private Integer columnShowOrder; + + /** + * 附加信息。 + */ + private String extra; + + /** + * 数值型字段精度。 + */ + private Integer numericPrecision; + + /** + * 数值型字段刻度。 + */ + private Integer numericScale; + + /** + * 字符型字段精度。 + */ + private Long stringPrecision; + + /** + * 缺省值。 + */ + private Object columnDefault; + + /** + * 数据库链接类型。该值为冗余字段,只是为了提升运行时效率。 + */ + private int dblinkType; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java new file mode 100644 index 00000000..c0a2423f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/DataSourceProvider.java @@ -0,0 +1,108 @@ +package com.orangeforms.common.dbutil.provider; + +/** + * 数据源操作的提供者接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface DataSourceProvider { + + /** + * 返回数据库链接类型,具体值可参考DblinkType常量类。 + * @return 返回数据库链接类型 + */ + int getDblinkType(); + + /** + * 返回Jdbc的配置对象。 + * + * @param configuration Jdbc 的配置数据,JSON格式。 + * @return Jdbc的配置对象。 + */ + JdbcConfig getJdbcConfig(String configuration); + + /** + * 获取当前数据库表meta列表数据的SQL语句。 + * + * @param searchString 表名的模糊匹配字符串。如果为空,则没有前缀规律。 + * @return 查询数据库表meta列表数据的SQL语句。 + */ + String getTableMetaListSql(String searchString); + + /** + * 获取当前数据库表meta数据的SQL语句。 + * + * @return 查询数据库表meta数据的SQL语句。 + */ + String getTableMetaSql(); + + /** + * 获取当前数据库指定表字段meta列表数据的SQL语句。 + * + * @return 查询指定表字段meta列表数据的SQL语句。 + */ + String getTableColumnMetaListSql(); + + /** + * 获取测试数据库连接的查询SQL。 + * + * @return 测试数据库连接的查询SQL + */ + default String getTestQuery() { + return "SELECT 'x'"; + } + + /** + * 为当前的SQL参数,加上分页部分。 + * + * @param sql SQL查询语句。 + * @param pageNum 页号,从1开始。 + * @param pageSize 每页数据量,如果为null,则取出后面所有数据。 + * @return 加上分页功能的SQL语句。 + */ + String makePageSql(String sql, Integer pageNum, Integer pageSize); + + /** + * 将数据表字段类型转换为Java字段类型。 + * + * @param columnType 数据表字段类型。 + * @param numericPrecision 数值精度。 + * @param numericScale 数值刻度。 + * @return 转换后的类型。 + */ + String convertColumnTypeToJavaType(String columnType, Integer numericPrecision, Integer numericScale); + + /** + * Having从句中,统计字段参与过滤时,是否可以直接使用别名。 + * + * @return 返回true,支持"HAVING sumOfColumn > 0",返回false,则为"HAVING sum(count) > 0"。 + */ + default boolean havingClauseUsingAlias() { + return true; + } + + /** + * SELECT的字段别名,是否需要加双引号,对于有些数据库,如果不加双引号,就会被数据库进行强制性的规则转义。 + * + * @return 返回true,SELECT grade_id "gradeId",否则 SELECT grade_id gradeId + */ + default boolean aliasWithQuotes() { + return false; + } + + /** + * 获取日期类型过滤条件语句。 + * + * @param columnName 字段名。 + * @param operator 操作符。 + * @return 过滤从句。 + */ + default String makeDateTimeFilterSql(String columnName, String operator) { + StringBuilder s = new StringBuilder(128); + if (columnName == null) { + columnName = ""; + } + return s.append(columnName).append(" ").append(operator).append(" ?").toString(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java new file mode 100644 index 00000000..031b9541 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/JdbcConfig.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.dbutil.provider; + +import lombok.Data; + +/** + * JDBC配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class JdbcConfig { + + /** + * 驱动名。由子类提供。 + */ + private String driver; + /** + * 连接池验证查询的语句。 + */ + private String validationQuery = "SELECT 'x'"; + /** + * Jdbc连接串,需要子类提供实现。 + */ + private String jdbcConnectionString; + /** + * 主机名。 + */ + private String host; + /** + * 端口号。 + */ + private Integer port; + /** + * 用户名。 + */ + private String username; + /** + * 密码。 + */ + private String password; + /** + * 数据库名。 + */ + private String database; + /** + * 模式名。 + */ + private String schema; + /** + * 连接池初始大小。 + */ + private int initialPoolSize = 5; + /** + * 连接池最小连接数。 + */ + private int minPoolSize = 5; + /** + * 连接池最大连接数。 + */ + private int maxPoolSize = 50; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java new file mode 100644 index 00000000..cc7558b2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlConfig.java @@ -0,0 +1,42 @@ +package com.orangeforms.common.dbutil.provider; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * MySQL JDBC配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class MySqlConfig extends JdbcConfig { + + /** + * JDBC 驱动名。 + */ + private String driver = "com.mysql.cj.jdbc.Driver"; + /** + * 数据库JDBC连接串的扩展部分。 + */ + private String extraParams = "?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"; + + /** + * 获取拼好后的JDBC连接串。 + * + * @return 拼好后的JDBC连接串。 + */ + @Override + public String getJdbcConnectionString() { + StringBuilder sb = new StringBuilder(256); + sb.append("jdbc:mysql://") + .append(getHost()) + .append(":") + .append(getPort()) + .append("/") + .append(getDatabase()) + .append(extraParams); + return sb.toString(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java new file mode 100644 index 00000000..e4e52bac --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/provider/MySqlProvider.java @@ -0,0 +1,112 @@ +package com.orangeforms.common.dbutil.provider; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.dbutil.constant.DblinkType; + +/** + * MySQL数据源的提供者实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class MySqlProvider implements DataSourceProvider { + + @Override + public int getDblinkType() { + return DblinkType.MYSQL; + } + + @Override + public JdbcConfig getJdbcConfig(String configuration) { + return JSON.parseObject(configuration, MySqlConfig.class); + } + + @Override + public String getTableMetaListSql(String searchString) { + StringBuilder sql = new StringBuilder(); + sql.append(this.getTableMetaListSql()); + if (StrUtil.isNotBlank(searchString)) { + sql.append(" AND table_name LIKE ?"); + } + return sql.append(" ORDER BY table_name").toString(); + } + + @Override + public String getTableMetaSql() { + return this.getTableMetaListSql() + " AND table_name = ?"; + } + + @Override + public String getTableColumnMetaListSql() { + return "SELECT " + + " column_name columnName, " + + " data_type columnType, " + + " column_type fullColumnType, " + + " column_comment columnComment, " + + " CASE WHEN column_key = 'PRI' THEN 1 ELSE 0 END AS primaryKey, " + + " is_nullable nullable, " + + " ordinal_position columnShowOrder, " + + " extra extra, " + + " CHARACTER_MAXIMUM_LENGTH stringPrecision, " + + " numeric_precision numericPrecision, " + + " COLUMN_DEFAULT columnDefault " + + "FROM " + + " information_schema.columns " + + "WHERE " + + " table_name = ?" + + " AND table_schema = (SELECT database()) " + + "ORDER BY ordinal_position"; + } + + @Override + public String makePageSql(String sql, Integer pageNum, Integer pageSize) { + if (pageSize == null) { + pageSize = 10; + } + int offset = pageNum > 0 ? (pageNum - 1) * pageSize : 0; + return sql + " LIMIT " + offset + "," + pageSize; + } + + @Override + public String convertColumnTypeToJavaType(String columnType, Integer numericPrecision, Integer numericScale) { + if (StrUtil.equalsAnyIgnoreCase(columnType, + "varchar", "char", "text", "longtext", "mediumtext", "tinytext", "enum", "json")) { + return ObjectFieldType.STRING; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "int", "mediumint", "smallint", "tinyint")) { + return ObjectFieldType.INTEGER; + } + if (StrUtil.equalsIgnoreCase(columnType, "bit")) { + return ObjectFieldType.BOOLEAN; + } + if (StrUtil.equalsIgnoreCase(columnType, "bigint")) { + return ObjectFieldType.LONG; + } + if (StrUtil.equalsIgnoreCase(columnType, "decimal")) { + return ObjectFieldType.BIG_DECIMAL; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "float", "double")) { + return ObjectFieldType.DOUBLE; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "date", "datetime", "timestamp", "time")) { + return ObjectFieldType.DATE; + } + if (StrUtil.equalsAnyIgnoreCase(columnType, "longblob", "blob")) { + return ObjectFieldType.BYTE_ARRAY; + } + return null; + } + + private String getTableMetaListSql() { + return "SELECT " + + " table_name tableName, " + + " table_comment tableComment, " + + " create_time createTime " + + "FROM " + + " information_schema.tables " + + "WHERE " + + " table_schema = DATABASE() "; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java new file mode 100644 index 00000000..a3ca1445 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dbutil/src/main/java/com/orangeforms/common/dbutil/util/DataSourceUtil.java @@ -0,0 +1,840 @@ +package com.orangeforms.common.dbutil.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.pool.DruidDataSourceFactory; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.FieldFilterType; +import com.orangeforms.common.core.exception.InvalidDblinkTypeException; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.core.util.MyDateUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.dbutil.constant.CustomDateValueType; +import com.orangeforms.common.dbutil.constant.DblinkType; +import com.orangeforms.common.dbutil.object.*; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.dbutil.provider.JdbcConfig; +import com.orangeforms.common.dbutil.provider.MySqlProvider; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectExpressionItem; +import net.sf.jsqlparser.statement.select.SelectItem; +import org.joda.time.DateTime; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 动态加载的数据源工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public abstract class DataSourceUtil { + + private final Lock lock = new ReentrantLock(); + private final Map datasourceMap = MapUtil.newHashMap(); + private static final Map PROVIDER_MAP = new HashMap<>(5); + protected final Map dblinkProviderMap = new ConcurrentHashMap<>(4); + + private static final String SQL_SELECT = " SELECT "; + private static final String SQL_SELECT_FROM = " SELECT * FROM ("; + private static final String SQL_AS_TMP = " ) tmp "; + private static final String SQL_ORDER_BY = " ORDER BY "; + private static final String SQL_AND = " AND "; + private static final String SQL_WHERE = " WHERE "; + private static final String LOG_PREPARING_FORMAT = "==> Preparing: {}"; + private static final String LOG_PARMS_FORMAT = "==> Parameters: {}"; + private static final String LOG_TOTAL_FORMAT = "<== Total: {}"; + + static { + PROVIDER_MAP.put(DblinkType.MYSQL, new MySqlProvider()); + } + + /** + * 由子类实现,根据dblinkId获取数据库链接类型的方法。 + * + * @param dblinkId 数据库链接Id。 + * @return 数据库链接类型。 + */ + protected abstract int getDblinkTypeByDblinkId(Long dblinkId); + + /** + * 由子类实现,根据dblinkId获取数据库链接配置信息的方法。 + * + * @param dblinkId 数据库链接Id。 + * @return 数据库链接配置信息。 + */ + protected abstract String getDblinkConfigurationByDblinkId(Long dblinkId); + + /** + * 获取指定数据库类型的Provider实现类。 + * + * @param dblinkType 数据库类型。 + * @return 指定数据库类型的Provider实现类。 + */ + public DataSourceProvider getProvider(Integer dblinkType) { + return PROVIDER_MAP.get(dblinkType); + } + + /** + * 获取指定数据库链接的Provider实现类。 + * + * @param dblinkId 数据库链接Id。 + * @return 指定数据库类型的Provider实现类。 + */ + public DataSourceProvider getProvider(Long dblinkId) { + int dblinkType = this.getDblinkTypeByDblinkId(dblinkId); + DataSourceProvider provider = PROVIDER_MAP.get(dblinkType); + if (provider == null) { + throw new InvalidDblinkTypeException(dblinkType); + } + return provider; + } + + /** + * 测试数据库链接。 + * + * @param dblinkId 数据库链接Id。 + */ + public void testConnection(Long dblinkId) throws Exception { + DataSourceProvider provider = this.getProvider(dblinkId); + this.query(dblinkId, provider.getTestQuery()); + } + + /** + * 通过JDBC方式测试链接。 + * + * @param databaseType 数据库类型。参考DblinkType常量值。 + * @param host 主机名。 + * @param port 端口号。 + * @param schemaName 模式名。 + * @param databaseName 数据库名。 + * @param username 用户名。 + * @param password 密码。 + */ + public static void testConnection( + int databaseType, + String host, + Integer port, + String schemaName, + String databaseName, + String username, + String password) { + StringBuilder urlBuilder = new StringBuilder(256); + String hostAndPort = host + ":" + port; + urlBuilder.append("jdbc:mysql://") + .append(hostAndPort) + .append("/") + .append(databaseName) + .append("?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"); + try { + Connection conn = DriverManager.getConnection(urlBuilder.toString(), username, password); + conn.close(); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new MyRuntimeException(e.getMessage()); + } + } + + /** + * 根据Dblink对象获取关联的数据源。如果不存在会创建该数据库连接池的数据源, + * 并保存到Map中缓存,下次调用时可直接返回。 + * + * @param dblinkId 数据库链接Id。 + * @return 关联的数据库连接池的数据源。 + */ + public DataSource getDataSource(Long dblinkId) throws Exception { + DataSource dataSource = datasourceMap.get(dblinkId); + if (dataSource != null) { + return dataSource; + } + int dblinkType = this.getDblinkTypeByDblinkId(dblinkId); + DataSourceProvider provider = PROVIDER_MAP.get(dblinkType); + if (provider == null) { + throw new InvalidDblinkTypeException(dblinkType); + } + DruidDataSource druidDataSource = null; + lock.lock(); + try { + dataSource = datasourceMap.get(dblinkId); + if (dataSource != null) { + return dataSource; + } + JdbcConfig jdbcConfig = provider.getJdbcConfig(this.getDblinkConfigurationByDblinkId(dblinkId)); + Properties properties = new Properties(); + druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties); + druidDataSource.setUrl(jdbcConfig.getJdbcConnectionString()); + druidDataSource.setDriverClassName(jdbcConfig.getDriver()); + druidDataSource.setValidationQuery(jdbcConfig.getValidationQuery()); + druidDataSource.setUsername(jdbcConfig.getUsername()); + druidDataSource.setPassword(jdbcConfig.getPassword()); + druidDataSource.setInitialSize(jdbcConfig.getInitialPoolSize()); + druidDataSource.setMinIdle(jdbcConfig.getMinPoolSize()); + druidDataSource.setMaxActive(jdbcConfig.getMaxPoolSize()); + druidDataSource.setConnectionErrorRetryAttempts(2); + druidDataSource.setTimeBetweenConnectErrorMillis(500); + druidDataSource.setBreakAfterAcquireFailure(true); + druidDataSource.init(); + datasourceMap.put(dblinkId, druidDataSource); + return druidDataSource; + } catch (Exception e) { + if (druidDataSource != null) { + druidDataSource.close(); + } + log.error("Failed to create DruidDatasource", e); + throw e; + } finally { + lock.unlock(); + } + } + + /** + * 关闭指定数据库链接Id关联的数据源,同时从缓存中移除该数据源对象。 + * + * @param dblinkId 数据库链接Id。 + */ + public void removeDataSource(Long dblinkId) { + lock.lock(); + try { + DataSource dataSource = datasourceMap.get(dblinkId); + if (dataSource == null) { + return; + } + ((DruidDataSource) dataSource).close(); + datasourceMap.remove(dblinkId); + } finally { + lock.unlock(); + } + } + + /** + * 获取指定数据源的数据库连接对象。 + * + * @param dblinkId 数据库链接Id。 + * @return 数据库连接对象。 + */ + public Connection getConnection(Long dblinkId) throws Exception { + DataSource dataSource = this.getDataSource(dblinkId); + return dataSource == null ? null : dataSource.getConnection(); + } + + /** + * 获取指定数据库链接的数据表列表。 + * + * @param dblinkId 数据库链接Id。 + * @param searchString 表名的模糊匹配字符串。如果为空,则没有前缀规律。 + * @return 数据表对象列表。 + */ + public List getTableList(Long dblinkId, String searchString) { + DataSourceProvider provider = this.getProvider(dblinkId); + List paramList = null; + if (StrUtil.isNotBlank(searchString)) { + paramList = new LinkedList<>(); + paramList.add("%" + searchString + "%"); + } + String querySql = provider.getTableMetaListSql(searchString); + try { + return this.query(dblinkId, querySql, paramList, SqlTable.class); + } catch (Exception e) { + log.error("Failed to call getTableList", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定数据库链接的数据表对象。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名称。 + * @return 数据表对象。 + */ + public SqlTable getTable(Long dblinkId, String tableName) { + DataSourceProvider provider = this.getProvider(dblinkId); + String querySql = provider.getTableMetaSql(); + List paramList = new LinkedList<>(); + paramList.add(tableName); + try { + return this.queryOne(dblinkId, querySql, paramList, SqlTable.class); + } catch (Exception e) { + log.error("Failed to call getTable", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定数据库链接下数据表的字段列表。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名称。 + * @return 数据表的字段列表。 + */ + public List getTableColumnList(Long dblinkId, String tableName) { + try { + DataSource dataSource = this.getDataSource(dblinkId); + try (Connection conn = dataSource.getConnection()) { + return this.getTableColumnList(dblinkId, conn, tableName); + } + } catch (Exception e) { + log.error("Failed to call getTableColumnList", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定数据库链接下数据表的字段列表。 + * + * @param dblinkId 数据库链接Id。 + * @param conn 数据库连接对象。 + * @param tableName 表名称。 + * @return 数据表的字段列表。 + */ + public List getTableColumnList(Long dblinkId, Connection conn, String tableName) { + DataSourceProvider provider = this.getProvider(dblinkId); + String querySql = provider.getTableColumnMetaListSql(); + List paramList = new LinkedList<>(); + paramList.add(tableName); + try { + List> dataList = this.query(conn, querySql, paramList); + return this.toTypedDataList(dataList, SqlTableColumn.class); + } catch (Exception e) { + log.error("Failed to call getTableColumnList", e); + throw new MyRuntimeException(e); + } + } + + /** + * 获取指定表的数据。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名。 + * @param datasetParam 数据集查询参数对象。 + * @return 表的数据结果。 + */ + public SqlResultSet> getTableDataList( + Long dblinkId, String tableName, DatasetParam datasetParam) throws Exception { + SqlTable table = this.getTable(dblinkId, tableName); + if (table == null) { + return null; + } + DataSourceProvider provider = this.getProvider(dblinkId); + if (datasetParam == null) { + datasetParam = new DatasetParam(); + } + String sql = "SELECT * FROM " + tableName; + if (CollUtil.isNotEmpty(datasetParam.getSelectColumnNameList())) { + sql = SQL_SELECT + StrUtil.join(",", datasetParam.getSelectColumnNameList()) + " FROM " + tableName; + } + Tuple2> filterTuple = this.buildWhereClauseByFilters(dblinkId, datasetParam.getFilter()); + sql += filterTuple.getFirst(); + List paramList = filterTuple.getSecond(); + String sqlCount = null; + MyPageParam pageParam = datasetParam.getPageParam(); + if (pageParam != null) { + net.sf.jsqlparser.statement.Statement statement = CCJSqlParserUtil.parse(sql); + Select select = (Select) statement; + PlainSelect selectBody = (PlainSelect) select.getSelectBody(); + List countSelectItems = new LinkedList<>(); + countSelectItems.add(new SelectExpressionItem(new Column("COUNT(1) AS CNT"))); + selectBody.setSelectItems(countSelectItems); + sqlCount = select.toString(); + sql = provider.makePageSql(sql, pageParam.getPageNum(), pageParam.getPageSize()); + } + return this.getDataListInternnally(dblinkId, provider, sqlCount, sql, datasetParam, paramList); + } + + /** + * 在指定数据库链接上执行查询语句,并返回指定映射对象类型的单条数据对象。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @param paramList 参数列表。 + * @param clazz 返回的映射对象Class类型。 + * @return 查询的结果对象。 + */ + public T queryOne(Long dblinkId, String query, List paramList, Class clazz) throws Exception { + List dataList = this.query(dblinkId, query, paramList, clazz); + return CollUtil.isEmpty(dataList) ? null : dataList.get(0); + } + + /** + * 在指定数据库链接上执行查询语句,并返回指定映射对象类型的数据列表。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @param paramList 参数列表。 + * @param clazz 返回的映射对象Class类型。 + * @return 查询的结果集。 + */ + public List query(Long dblinkId, String query, List paramList, Class clazz) throws Exception { + List> dataList = this.query(dblinkId, query, paramList); + return this.toTypedDataList(dataList, clazz); + } + + /** + * 在指定数据库链接上执行查询语句。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @return 查询的结果集。 + */ + public List> query(Long dblinkId, String query) throws Exception { + DataSource dataSource = this.getDataSource(dblinkId); + try (Connection conn = dataSource.getConnection()) { + return this.query(conn, query); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw e; + } + } + + /** + * 在指定数据库链接上执行查询语句。 + * + * @param dblinkId 数据库链接Id。 + * @param query 待执行的SQL语句。 + * @param paramList 参数列表。 + * @return 查询的结果集。 + */ + public List> query(Long dblinkId, String query, List paramList) throws Exception { + DataSource dataSource = this.getDataSource(dblinkId); + try (Connection conn = dataSource.getConnection()) { + return this.query(conn, query, paramList); + } + } + + /** + * 计算过滤从句和过滤参数。 + * + * @param dblinkId 数据库链接Id。 + * @param filter 过滤参数列表。 + * @return 返回的Tuple对象的第一个参数是WHERE从句,第二个参数是过滤从句用到的参数列表。 + */ + public Tuple2> buildWhereClauseByFilters(Long dblinkId, DatasetFilter filter) { + filter = this.normalizeFilter(filter); + if (CollUtil.isEmpty(filter)) { + return new Tuple2<>("", null); + } + DataSourceProvider provider = this.getProvider(dblinkId); + StringBuilder where = new StringBuilder(); + int i = 0; + List paramList = new LinkedList<>(); + for (DatasetFilter.FilterInfo filterInfo : filter) { + if (i++ == 0) { + where.append(SQL_WHERE); + } else { + where.append(SQL_AND); + } + this.doBuildWhereClauseByFilter(filterInfo, provider, where, paramList); + } + return new Tuple2<>(where.toString(), paramList); + } + + private void doBuildWhereClauseByFilter( + DatasetFilter.FilterInfo filterInfo, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + where.append(filterInfo.getParamName()); + if (filterInfo.getFilterType().equals(FieldFilterType.EQUAL)) { + this.doBuildWhereClauseByEqualFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.NOT_EQUAL)) { + where.append(" <> ?"); + paramList.add(filterInfo.getParamValue()); + } else if (filterInfo.getFilterType().equals(FieldFilterType.GE)) { + this.doBuildWhereClauseByGeFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.GT)) { + this.doBuildWhereClauseByGtFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.LE)) { + this.doBuildWhereClauseByLeFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.LT)) { + this.doBuildWhereClauseByLtFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.BETWEEN)) { + this.doBuildWhereClauseByBetweenFilter(filterInfo, provider, where, paramList); + } else if (filterInfo.getFilterType().equals(FieldFilterType.LIKE)) { + where.append(" LIKE ?"); + paramList.add("%" + filterInfo.getParamValue() + "%"); + } else if (filterInfo.getFilterType().equals(FieldFilterType.IN)) { + where.append(" IN ("); + where.append(StrUtil.repeatAndJoin("?", filterInfo.getParamValueList().size(), ",")); + where.append(")"); + paramList.addAll(filterInfo.getParamValueList()); + } else if (filterInfo.getFilterType().equals(FieldFilterType.NOT_IN)) { + where.append(" NOT IN ("); + where.append(StrUtil.repeatAndJoin("?", filterInfo.getParamValueList().size(), ",")); + where.append(")"); + paramList.addAll(filterInfo.getParamValueList()); + } else if (filterInfo.getFilterType().equals(FieldFilterType.IS_NOT_NULL)) { + where.append(" IS NOT NULL"); + } else if (filterInfo.getFilterType().equals(FieldFilterType.IS_NULL)) { + where.append(" IS NULL"); + } + } + + private void doBuildWhereClauseByEqualFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + String beginDateTime = this.getBeginDateTime(filter.getParamValue().toString(), filter.getDateRange()); + String endDateTime = this.getEndDateTime(filter.getParamValue().toString(), filter.getDateRange()); + where.append(provider.makeDateTimeFilterSql(null, ">=")); + where.append(SQL_AND); + where.append(provider.makeDateTimeFilterSql(filter.getParamName(), "<=")); + paramList.add(beginDateTime); + paramList.add(endDateTime); + } else { + where.append(" = ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByGeFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, ">=")); + paramList.add(this.getBeginDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + paramList.add(filter.getParamValue()); + where.append(" >= ?"); + } + } + + private void doBuildWhereClauseByGtFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, ">")); + paramList.add(this.getEndDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + where.append(" > ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByLeFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, "<=")); + paramList.add(this.getEndDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + where.append(" <= ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByLtFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + where.append(provider.makeDateTimeFilterSql(null, "<")); + paramList.add(this.getBeginDateTime(filter.getParamValue().toString(), filter.getDateRange())); + } else { + where.append(" < ?"); + paramList.add(filter.getParamValue()); + } + } + + private void doBuildWhereClauseByBetweenFilter( + DatasetFilter.FilterInfo filter, + DataSourceProvider provider, + StringBuilder where, + List paramList) { + if (CollUtil.isEmpty(filter.getParamValueList())) { + return; + } + if (BooleanUtil.isTrue(filter.getDateValueFilter())) { + Object[] filterArray = filter.getParamValueList().toArray(); + where.append(provider.makeDateTimeFilterSql(null, ">=")); + paramList.add(this.getBeginDateTime(filterArray[0].toString(), filter.getDateRange())); + where.append(SQL_AND); + where.append(filter.getParamName()); + where.append(provider.makeDateTimeFilterSql(null, "<=")); + paramList.add(this.getEndDateTime(filterArray[1].toString(), filter.getDateRange())); + } else { + where.append(" BETWEEN ? AND ?"); + paramList.add(filter.getParamValueList()); + } + } + + private SqlResultSet> getDataListInternnally( + Long dblinkId, + DataSourceProvider provider, + String sqlCount, + String sql, + DatasetParam datasetParam, + List paramList) throws Exception { + Long totalCount = 0L; + SqlResultSet> resultSet = null; + try (Connection connection = this.getConnection(dblinkId)) { + boolean ignoreQueryData = false; + if (sqlCount != null) { + Map data = this.query(connection, sqlCount, paramList).get(0); + String key = data.entrySet().iterator().next().getKey(); + totalCount = (Long) data.get(key); + if (totalCount == 0L) { + ignoreQueryData = true; + } + } + if (!ignoreQueryData) { + if (datasetParam.getOrderBy() != null) { + sql += SQL_ORDER_BY + datasetParam.getOrderBy(); + } + resultSet = this.queryWithMeta(connection, sql, paramList); + resultSet.setTotalCount(totalCount); + } + } + return resultSet == null ? new SqlResultSet<>() : resultSet; + } + + private List> query(Connection conn, String query) throws SQLException { + try (Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery(query)) { + log.info(LOG_PREPARING_FORMAT, query); + List> resultList = this.fetchResult(rs); + log.info(LOG_TOTAL_FORMAT, resultList.size()); + return resultList; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } + } + + private List> query(Connection conn, String query, List paramList) throws SQLException { + if (CollUtil.isEmpty(paramList)) { + return this.query(conn, query); + } + ResultSet rs = null; + try (PreparedStatement stat = conn.prepareStatement(query)) { + for (int i = 0; i < paramList.size(); i++) { + stat.setObject(i + 1, paramList.get(i)); + } + rs = stat.executeQuery(); + log.info(LOG_PREPARING_FORMAT, query); + List> resultList = this.fetchResult(rs); + log.info(LOG_TOTAL_FORMAT, resultList.size()); + return resultList; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } finally { + if (rs != null) { + try { + rs.close(); + } catch (Exception e) { + log.error("Failed to call rs.close", e); + } + } + } + } + + private SqlResultSet> queryWithMeta( + Connection connection, String query, List paramList) throws SQLException { + if (CollUtil.isEmpty(paramList)) { + try (Statement stat = connection.createStatement(); + ResultSet rs = stat.executeQuery(query)) { + log.info(LOG_PREPARING_FORMAT, query); + SqlResultSet> resultSet = this.fetchResultWithMeta(rs); + log.info(LOG_TOTAL_FORMAT, resultSet.getDataList() == null ? 0 : resultSet.getDataList().size()); + return resultSet; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } + } + ResultSet rs = null; + try (PreparedStatement stat = connection.prepareStatement(query)) { + for (int i = 0; i < paramList.size(); i++) { + stat.setObject(i + 1, paramList.get(i)); + } + rs = stat.executeQuery(); + log.info(LOG_PREPARING_FORMAT, query); + SqlResultSet> resultSet = this.fetchResultWithMeta(rs); + log.info(LOG_TOTAL_FORMAT, resultSet.getDataList() == null ? 0 : resultSet.getDataList().size()); + return resultSet; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw e; + } finally { + if (rs != null) { + try { + rs.close(); + } catch (Exception e) { + log.error("Failed to call rs.close", e); + } + } + } + } + + private List> fetchResult(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + List> resultList = new LinkedList<>(); + while (rs.next()) { + JSONObject rowData = new JSONObject(); + for (int i = 0; i < columnCount; i++) { + rowData.put(metaData.getColumnLabel(i + 1), rs.getObject(i + 1)); + } + resultList.add(rowData); + } + return resultList; + } + + private SqlResultSet> fetchResultWithMeta(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + List columnMetaList = new LinkedList<>(); + int columnCount = metaData.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + SqlTableColumn tableColumn = new SqlTableColumn(); + String columnLabel = metaData.getColumnLabel(i + 1); + tableColumn.setColumnName(columnLabel); + tableColumn.setColumnType(metaData.getColumnTypeName(i + 1)); + columnMetaList.add(tableColumn); + } + List> resultList = new LinkedList<>(); + while (rs.next()) { + JSONObject rowData = new JSONObject(); + for (int i = 0; i < columnCount; i++) { + rowData.put(metaData.getColumnLabel(i + 1), rs.getObject(i + 1)); + } + resultList.add(rowData); + } + return new SqlResultSet<>(columnMetaList, resultList); + } + + private List toTypedDataList(List> dataList, Class clazz) { + return MyModelUtil.mapToBeanList(dataList, clazz); + } + + private String getBeginDateTime(String dateValueType, String dateRange) { + DateTime now = DateTime.now(); + switch (dateValueType) { + case CustomDateValueType.CURRENT_DAY: + return MyDateUtil.getBeginTimeOfDayWithShort(now); + case CustomDateValueType.CURRENT_WEEK: + return MyDateUtil.getBeginDateTimeOfWeek(now); + case CustomDateValueType.CURRENT_MONTH: + return MyDateUtil.getBeginDateTimeOfMonth(now); + case CustomDateValueType.CURRENT_YEAR: + return MyDateUtil.getBeginDateTimeOfYear(now); + case CustomDateValueType.CURRENT_QUARTER: + return MyDateUtil.getBeginDateTimeOfQuarter(now); + case CustomDateValueType.LAST_DAY: + return MyDateUtil.getBeginTimeOfDay(now.minusDays(1)); + case CustomDateValueType.LAST_WEEK: + return MyDateUtil.getBeginDateTimeOfWeek(now.minusWeeks(1)); + case CustomDateValueType.LAST_MONTH: + return MyDateUtil.getBeginDateTimeOfMonth(now.minusMonths(1)); + case CustomDateValueType.LAST_YEAR: + return MyDateUtil.getBeginDateTimeOfYear(now.minusYears(1)); + case CustomDateValueType.LAST_QUARTER: + return MyDateUtil.getBeginDateTimeOfQuarter(now.minusMonths(3)); + default: + break; + } + // 执行到这里,基本就是自定义日期数据了 + if (StrUtil.isBlank(dateRange)) { + return dateValueType; + } + DateTime dateValue = MyDateUtil.toDateTimeWithoutMs(dateValueType); + switch (dateRange) { + case "year": + return MyDateUtil.getBeginDateTimeOfYear(dateValue); + case "month": + return MyDateUtil.getBeginDateTimeOfMonth(dateValue); + case "week": + return MyDateUtil.getBeginDateTimeOfWeek(dateValue); + case "date": + return MyDateUtil.getBeginTimeOfDayWithShort(dateValue); + default: + break; + } + return dateValueType; + } + + private String getEndDateTime(String dateValueType, String dateRange) { + DateTime now = DateTime.now(); + switch (dateValueType) { + case CustomDateValueType.CURRENT_DAY: + return MyDateUtil.getEndTimeOfDayWithShort(now); + case CustomDateValueType.CURRENT_WEEK: + return MyDateUtil.getEndDateTimeOfWeek(now); + case CustomDateValueType.CURRENT_MONTH: + return MyDateUtil.getEndDateTimeOfMonth(now); + case CustomDateValueType.CURRENT_YEAR: + return MyDateUtil.getEndDateTimeOfYear(now); + case CustomDateValueType.CURRENT_QUARTER: + return MyDateUtil.getEndDateTimeOfQuarter(now); + case CustomDateValueType.LAST_DAY: + return MyDateUtil.getEndTimeOfDay(now.minusDays(1)); + case CustomDateValueType.LAST_WEEK: + return MyDateUtil.getEndDateTimeOfWeek(now.minusWeeks(1)); + case CustomDateValueType.LAST_MONTH: + return MyDateUtil.getEndDateTimeOfMonth(now.minusMonths(1)); + case CustomDateValueType.LAST_YEAR: + return MyDateUtil.getEndDateTimeOfYear(now.minusYears(1)); + case CustomDateValueType.LAST_QUARTER: + return MyDateUtil.getEndDateTimeOfQuarter(now.minusMonths(3)); + default: + break; + } + // 执行到这里,基本就是自定义日期数据了 + if (StrUtil.isBlank(dateRange)) { + return dateValueType; + } + DateTime dateValue = MyDateUtil.toDateTimeWithoutMs(dateValueType); + switch (dateRange) { + case "year": + return MyDateUtil.getEndDateTimeOfYear(dateValue); + case "month": + return MyDateUtil.getEndDateTimeOfMonth(dateValue); + case "week": + return MyDateUtil.getEndDateTimeOfWeek(dateValue); + case "date": + return MyDateUtil.getEndTimeOfDayWithShort(dateValue); + default: + break; + } + return dateValueType; + } + + private DatasetFilter normalizeFilter(DatasetFilter filter) { + if (CollUtil.isEmpty(filter)) { + return filter; + } + DatasetFilter normalizedFilter = new DatasetFilter(); + for (DatasetFilter.FilterInfo filterInfo : filter) { + if (filterInfo.getFilterType().equals(FieldFilterType.IS_NULL) + || filterInfo.getFilterType().equals(FieldFilterType.IS_NOT_NULL) + || filterInfo.getParamValue() != null + || filterInfo.getParamValueList() != null) { + normalizedFilter.add(filterInfo); + } + } + return normalizedFilter; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-dict/pom.xml new file mode 100644 index 00000000..c2fc5d2d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/pom.xml @@ -0,0 +1,31 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-dict + + + + com.orangeforms + common-redis + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java new file mode 100644 index 00000000..3076abfa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/constant/GlobalDictItemStatus.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.dict.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 全局字典项目数据状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class GlobalDictItemStatus { + + /** + * 正常。 + */ + public static final int NORMAL = 0; + /** + * 禁用。 + */ + public static final int DISABLED = 1; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(NORMAL, "正常"); + DICT_MAP.put(DISABLED, "禁用"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalDictItemStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java new file mode 100644 index 00000000..640491b6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictItemMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.GlobalDictItem; + +/** + * 全局字典项目数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictItemMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java new file mode 100644 index 00000000..f924430a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/GlobalDictMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.GlobalDict; + +/** + * 全局字典数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java new file mode 100644 index 00000000..8a744d02 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictItemMapper.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 租户全局字典项目数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictItemMapper extends BaseDaoMapper { + + /** + * 批量插入。 + * + * @param dictItemList 字典条目列表。 + */ + @Insert("") + void insertList(@Param("dictItemList") List dictItemList); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java new file mode 100644 index 00000000..6735d704 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dao/TenantGlobalDictMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.dict.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.dict.model.TenantGlobalDict; + +/** + * 租户全局字典数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java new file mode 100644 index 00000000..564655d7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictDto.java @@ -0,0 +1,40 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.UpdateGroup; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 全局系统字典Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典Dto") +@Data +public class GlobalDictDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long dictId; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + @NotBlank(message = "数据验证失败,字典编码不能为空!") + private String dictCode; + + /** + * 字典中文名称。 + */ + @Schema(description = "字典中文名称") + @NotBlank(message = "数据验证失败,字典中文名称不能为空!") + private String dictName; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java new file mode 100644 index 00000000..e80a934f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/GlobalDictItemDto.java @@ -0,0 +1,54 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.UpdateGroup; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 全局系统字典项目Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典项目Dto") +@Data +public class GlobalDictItemDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long id; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + @NotBlank(message = "数据验证失败,字典编码不能为空!") + private String dictCode; + + /** + * 字典数据项Id。 + */ + @Schema(description = "字典数据项Id") + @NotNull(message = "数据验证失败,字典数据项Id不能为空!") + private String itemId; + + /** + * 字典数据项名称。 + */ + @Schema(description = "字典数据项名称") + @NotBlank(message = "数据验证失败,字典数据项名称不能为空!") + private String itemName; + + /** + * 显示顺序(数值越小越靠前)。 + */ + @Schema(description = "显示顺序") + @NotNull(message = "数据验证失败,显示顺序不能为空!") + private Integer showOrder; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java new file mode 100644 index 00000000..63f55953 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictDto.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典Dto") +@EqualsAndHashCode(callSuper = true) +@Data +public class TenantGlobalDictDto extends GlobalDictDto { + + /** + * 是否为所有租户的通用字典。 + */ + @Schema(description = "是否为所有租户的通用字典") + private Boolean tenantCommon; + + /** + * 租户的非公用字典的初始化字典数据。 + */ + @Schema(description = "租户的非公用字典的初始化字典数据") + private String initialData; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java new file mode 100644 index 00000000..f6ac99a6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/dto/TenantGlobalDictItemDto.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.dict.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典项目Dto。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典项目Dto") +@EqualsAndHashCode(callSuper = true) +@Data +public class TenantGlobalDictItemDto extends GlobalDictItemDto { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java new file mode 100644 index 00000000..ffd91552 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDict.java @@ -0,0 +1,66 @@ +package com.orangeforms.common.dict.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_global_dict") +public class GlobalDict { + + /** + * 主键Id。 + */ + @TableId(value = "dict_id") + private Long dictId; + + /** + * 字典编码。 + */ + @TableField(value = "dict_code") + private String dictCode; + + /** + * 字典中文名称。 + */ + @TableField(value = "dict_name") + private String dictName; + + /** + * 更新用户名。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 创建用户Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 逻辑删除字段。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java new file mode 100644 index 00000000..fa73d48f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/GlobalDictItem.java @@ -0,0 +1,83 @@ +package com.orangeforms.common.dict.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典项目实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_global_dict_item") +public class GlobalDictItem { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 字典编码。 + */ + @TableField(value = "dict_code") + private String dictCode; + + /** + * 字典数据项Id。 + */ + @TableField(value = "item_id") + private String itemId; + + /** + * 字典数据项名称。 + */ + @TableField(value = "item_name") + private String itemName; + + /** + * 显示顺序(数值越小越靠前)。 + */ + @TableField(value = "show_order") + private Integer showOrder; + + /** + * 字典状态。具体值引用DictItemStatus常量类。 + */ + private Integer status; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建用户Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新用户名。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 逻辑删除字段。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java new file mode 100644 index 00000000..3aa846d0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDict.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.dict.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName(value = "zz_tenant_global_dict") +public class TenantGlobalDict extends GlobalDict { + + /** + * 是否为所有租户的通用字典。 + */ + @TableField(value = "tenant_common") + private Boolean tenantCommon; + + /** + * 租户的非公用字典的初始化字典数据。 + */ + @TableField(value = "initial_data") + private String initialData; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java new file mode 100644 index 00000000..bf5b73b0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/model/TenantGlobalDictItem.java @@ -0,0 +1,23 @@ +package com.orangeforms.common.dict.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典项目实体类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName(value = "zz_tenant_global_dict_item") +public class TenantGlobalDictItem extends GlobalDictItem { + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java new file mode 100644 index 00000000..66750ff7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictItemService.java @@ -0,0 +1,92 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.GlobalDictItem; + +import java.io.Serializable; +import java.util.List; + +/** + * 全局字典项目数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictItemService extends IBaseService { + + /** + * 保存新增的全局字典项目。 + * + * @param globalDictItem 新字典项目对象。 + * @return 保存后的对象。 + */ + GlobalDictItem saveNew(GlobalDictItem globalDictItem); + + /** + * 更新全局字典项目对象。 + * + * @param globalDictItem 更新的全局字典项目对象。 + * @param originalGlobalDictItem 原有的全局字典项目对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(GlobalDictItem globalDictItem, GlobalDictItem originalGlobalDictItem); + + /** + * 更新字典条目的编码。 + * + * @param oldCode 原有编码。 + * @param newCode 新编码。 + */ + void updateNewCode(String oldCode, String newCode); + + /** + * 更新字典条目的状态。 + * + * @param globalDictItem 字典项目对象。 + * @param status 状态值。 + */ + void updateStatus(GlobalDictItem globalDictItem, Integer status); + + /** + * 删除指定字典项目。 + * + * @param globalDictItem 待删除字典项目。 + * @return 成功返回true,否则false。 + */ + boolean remove(GlobalDictItem globalDictItem); + + /** + * 判断指定的编码和项目Id是否存在。 + * + * @param dictCode 字典编码。 + * @param itemId 项目Id。 + * @return true存在,否则false。 + */ + boolean existDictCodeAndItemId(String dictCode, Serializable itemId); + + /** + * 根据字典编码和项目Id获取指定字段项目对象。 + * + * @param dictCode 字典编码。 + * @param itemId 项目Id。 + * @return 字典项目对象。 + */ + GlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId); + + /** + * 查询数据字典项目列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序字符串,如果为空,则按照showOrder升序排序。 + * @return 查询结果列表。 + */ + List getGlobalDictItemList(GlobalDictItem filter, String orderBy); + + /** + * 查询指定字典编码的数据字典项目列表。查询结果按照showOrder升序排序。 + * + * @param dictCode 过滤对象。 + * @return 查询结果列表。 + */ + List getGlobalDictItemListByDictCode(String dictCode); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java new file mode 100644 index 00000000..2eaadcf2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/GlobalDictService.java @@ -0,0 +1,108 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 全局字典数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface GlobalDictService extends IBaseService { + + /** + * 保存全局字典对象。 + * + * @param globalDict 全局字典对象。 + * @return 保存后的字典对象。 + */ + GlobalDict saveNew(GlobalDict globalDict); + + /** + * 更新全局字典对象。 + * + * @param globalDict 更新的全局字典对象。 + * @param originalGlobalDict 原有的全局字典对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(GlobalDict globalDict, GlobalDict originalGlobalDict); + + /** + * 删除全局字典对象,以及其关联的字典项目数据。 + * + * @param dictId 全局字典Id。 + * @return 是否删除成功。 + */ + boolean remove(Long dictId); + + /** + * 获取全局字典列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序条件。 + * @return 查询结果集列表。 + */ + List getGlobalDictList(GlobalDict filter, String orderBy); + + /** + * 判断字典编码是否存在。 + * + * @param dictCode 字典编码。 + * @return true表示存在,否则false。 + */ + boolean existDictCode(String dictCode); + + /** + * 判断指定字典编码的字典项目是否存在。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemId 字典项目Id。 + * @return true表示存在,否则false。 + */ + boolean existDictItemFromCache(String dictCode, Serializable itemId); + + /** + * 从缓存中获取指定编码的字典项目列表。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + List getGlobalDictItemListFromCache(String dictCode, Set itemIds); + + /** + * 从缓存中获取指定编码的字典项目列表。返回的结果Map中,键是itemId,值是itemName。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + Map getGlobalDictItemDictMapFromCache(String dictCode, Set itemIds); + + /** + * 强制同步指定字典编码的全部字典项目到缓存。 + * + * @param dictCode 字典编码。 + */ + void reloadCachedData(String dictCode); + + /** + * 从缓存中移除指定字典编码的数据。 + * + * @param dictCode 字典编码。 + */ + void removeCache(String dictCode); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java new file mode 100644 index 00000000..74d3f5fa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictItemService.java @@ -0,0 +1,115 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; + +import java.io.Serializable; +import java.util.List; + +/** + * 租户全局字典项目数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictItemService extends IBaseService { + + /** + * 保存新增的租户字典项目。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 新字典项目对象。 + * @return 保存后的对象。 + */ + TenantGlobalDictItem saveNew(TenantGlobalDict tenantGlobalDict, TenantGlobalDictItem tenantGlobalDictItem); + + /** + * 批量新增的租户字典项目。 + * + * @param dictItemList 字典项对象列表。 + */ + void saveNewBatch(List dictItemList); + + /** + * 更新租户字典项目对象。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 更新的全局字典项目对象。 + * @param originalTenantGlobalDictItem 原有的全局字典项目对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update( + TenantGlobalDict tenantGlobalDict, + TenantGlobalDictItem tenantGlobalDictItem, + TenantGlobalDictItem originalTenantGlobalDictItem); + + /** + * 更新字典条目的编码。 + * + * @param oldCode 原有编码。 + * @param newCode 新编码。 + */ + void updateNewCode(String oldCode, String newCode); + + /** + * 更新字典条目的状态。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 字典项目对象。 + * @param status 状态值。 + */ + void updateStatus(TenantGlobalDict tenantGlobalDict, TenantGlobalDictItem tenantGlobalDictItem, Integer status); + + /** + * 删除指定租户字典项目。 + * + * @param tenantGlobalDict 字典对象。 + * @param tenantGlobalDictItem 待删除字典项目。 + * @return 成功返回true,否则false。 + */ + boolean remove(TenantGlobalDict tenantGlobalDict, TenantGlobalDictItem tenantGlobalDictItem); + + /** + * 判断指定字典的项目Id是否存在。如果是租户非公用字典,会基于租户Id进行过滤。 + * + * @param tenantGlobalDict 字典对象。 + * @param itemId 项目Id。 + * @return true存在,否则false。 + */ + boolean existDictCodeAndItemId(TenantGlobalDict tenantGlobalDict, Serializable itemId); + + /** + * 判断指定租户的编码是否已经存在字典数据。 + * + * @param dictCode 字典编码。 + * @return true存在,否则false。 + */ + boolean existDictCode(String dictCode); + + /** + * 根据租户字典编码和项目Id获取指定字段项目对象。 + * + * @param dictCode 字典编码。 + * @param itemId 项目Id。 + * @return 字典项目对象。 + */ + TenantGlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId); + + /** + * 查询租户数据字典项目列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序字符串,如果为空,则按照showOrder升序排序。 + * @return 查询结果列表。 + */ + List getGlobalDictItemList(TenantGlobalDictItem filter, String orderBy); + + /** + * 查询指定字典的租户数据字典项目列表。如果是租户非公用字典,会仅仅返回该租户的字典数据列表。按照showOrder升序排序。 + * + * @param tenantGlobalDict 编码字典对象。 + * @return 查询结果列表。 + */ + List getGlobalDictItemList(TenantGlobalDict tenantGlobalDict); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java new file mode 100644 index 00000000..3c02c46c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/TenantGlobalDictService.java @@ -0,0 +1,137 @@ +package com.orangeforms.common.dict.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 租户全局字典数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface TenantGlobalDictService extends IBaseService { + + /** + * 保存租户全局字典对象。 + * + * @param tenantGlobalDict 全局租户字典对象。 + * @param tenantIdSet 租户Id集合。 + * @return 保存后的字典对象。 + */ + TenantGlobalDict saveNew(TenantGlobalDict tenantGlobalDict, Set tenantIdSet); + + /** + * 更新租户全局字典对象。 + * + * @param tenantGlobalDict 更新的租户全局字典对象。 + * @param originalTenantGlobalDict 原有的租户全局字典对象。 + * @return 更新成功返回true,否则false。 + */ + boolean update(TenantGlobalDict tenantGlobalDict, TenantGlobalDict originalTenantGlobalDict); + + /** + * 删除租户全局字典对象,以及其关联的字典项目数据。 + * + * @param dictId 全局字典Id。 + * @return 是否删除成功。 + */ + boolean remove(Long dictId); + + /** + * 获取全局字典列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序条件。 + * @return 查询结果集列表。 + */ + List getGlobalDictList(TenantGlobalDict filter, String orderBy); + + /** + * 判断租户字典编码是否存在。 + * + * @param dictCode 字典编码。 + * @return true表示存在,否则false。 + */ + boolean existDictCode(String dictCode); + + /** + * 根据字典编码获取全局字典编码对象。 + * + * @param dictCode 字典编码。 + * @return 查询后的字典对象。 + */ + TenantGlobalDict getTenantGlobalDictByDictCode(String dictCode); + + /** + * 从缓存中中获取指定字典数据。如果缓存中不存在,会从数据库读取并同步到缓存。 + * + * @param dictCode 字典编码。 + * @return 查询到的字段对象。 + */ + TenantGlobalDict getTenantGlobalDictFromCache(String dictCode); + + /** + * 从缓存中获取指定编码的字典项目列表。 + * 如果是租户非公用字典,会仅仅返回该租户的字典数据列表。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param tenantGlobalDict 编码字典对象。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + List getGlobalDictItemListFromCache(TenantGlobalDict tenantGlobalDict, Set itemIds); + + /** + * 从缓存中获取指定编码的字典项目列表。返回的结果Map中,键是itemId,值是itemName。 + * 如果是租户非公用字典,会仅仅返回该租户的字典数据列表。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param tenantGlobalDict 编码字典对象。 + * @param itemIds 字典项目Id集合。 + * @return 查询结果列表。 + */ + Map getGlobalDictItemDictMapFromCache(TenantGlobalDict tenantGlobalDict, Set itemIds); + + /** + * 强制同步指定所有租户通用字典编码的全部字典项目到缓存。 + * 如果是租户非公用字典,会仅仅返回该租户的字典数据列表。 + * + * @param tenantGlobalDict 编码字典对象。 + */ + void reloadCachedData(TenantGlobalDict tenantGlobalDict); + + /** + * 重置所有非公用租户编码字典的数据到缓存。 + * 该方法会将指定编码字典中,所有租户的缓存全部重新加载。一般用于系统故障,或大促活动的数据预热。 + * + * @param tenantGlobalDict 非公用编码字典对象。 + */ + void reloadAllTenantCachedData(TenantGlobalDict tenantGlobalDict); + + /** + * 从缓存中移除指定字典编码的数据。 + * 该方法的实现内部会判断是否为公用字典,还是租户可修改的非公用字典。 + * + * @param tenantGlobalDict 字典编码。 + */ + void removeCache(TenantGlobalDict tenantGlobalDict); + + /** + * 判断指定字典编码的字典项目是否存在。 + * 该方法通常会在业务主表中调用,为了提升整体运行时效率,该方法会从缓存中获取,如果缓存为空, + * 会从数据库读取指定编码的字典数据,并同步到缓存。 + * + * @param dictCode 字典编码。 + * @param itemId 字典项目Id。 + * @return true表示存在,否则false。 + */ + boolean existDictItemFromCache(String dictCode, Serializable itemId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java new file mode 100644 index 00000000..662511a3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictItemServiceImpl.java @@ -0,0 +1,143 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.GlobalDictItemMapper; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictItemService; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 全局字典项目数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE) +@Service("globalDictItemService") +public class GlobalDictItemServiceImpl + extends BaseService implements GlobalDictItemService { + + @Autowired + private GlobalDictItemMapper globalDictItemMapper; + @Autowired + private GlobalDictService globalDictService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return globalDictItemMapper; + } + + @Override + public GlobalDictItem saveNew(GlobalDictItem globalDictItem) { + globalDictService.removeCache(globalDictItem.getDictCode()); + globalDictItem.setId(idGenerator.nextLongId()); + globalDictItem.setDeletedFlag(GlobalDeletedFlag.NORMAL); + globalDictItem.setStatus(GlobalDictItemStatus.NORMAL); + globalDictItem.setCreateUserId(TokenData.takeFromRequest().getUserId()); + globalDictItem.setUpdateUserId(globalDictItem.getCreateUserId()); + globalDictItem.setCreateTime(new Date()); + globalDictItem.setUpdateTime(globalDictItem.getCreateTime()); + globalDictItemMapper.insert(globalDictItem); + return globalDictItem; + } + + @Override + public boolean update(GlobalDictItem globalDictItem, GlobalDictItem originalGlobalDictItem) { + globalDictService.removeCache(globalDictItem.getDictCode()); + // 该方法不能直接修改字典状态。 + globalDictItem.setStatus(originalGlobalDictItem.getStatus()); + globalDictItem.setCreateUserId(originalGlobalDictItem.getCreateUserId()); + globalDictItem.setCreateTime(originalGlobalDictItem.getCreateTime()); + globalDictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + globalDictItem.setUpdateTime(new Date()); + return globalDictItemMapper.updateById(globalDictItem) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateNewCode(String oldCode, String newCode) { + GlobalDictItem globalDictItem = new GlobalDictItem(); + globalDictItem.setDictCode(newCode); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(GlobalDictItem::getDictCode, oldCode); + globalDictItemMapper.update(globalDictItem, queryWrapper); + } + + @Override + public void updateStatus(GlobalDictItem globalDictItem, Integer status) { + globalDictService.removeCache(globalDictItem.getDictCode()); + globalDictItem.setStatus(status); + globalDictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + globalDictItem.setUpdateTime(new Date()); + globalDictItemMapper.updateById(globalDictItem); + } + + @Override + public boolean remove(GlobalDictItem globalDictItem) { + globalDictService.removeCache(globalDictItem.getDictCode()); + return this.removeById(globalDictItem.getId()); + } + + @Override + public boolean existDictCodeAndItemId(String dictCode, Serializable itemId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(GlobalDictItem::getDictCode, dictCode); + queryWrapper.eq(GlobalDictItem::getItemId, itemId.toString()); + return globalDictItemMapper.selectCount(queryWrapper) > 0; + } + + @Override + public GlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(GlobalDictItem::getDictCode, dictCode); + queryWrapper.eq(GlobalDictItem::getItemId, itemId.toString()); + return globalDictItemMapper.selectOne(queryWrapper); + } + + @Override + public List getGlobalDictItemList(GlobalDictItem filter, String orderBy) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } else { + queryWrapper.orderByAsc(GlobalDictItem::getShowOrder); + } + return globalDictItemMapper.selectList(queryWrapper); + } + + @Override + public List getGlobalDictItemListByDictCode(String dictCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(GlobalDictItem::getDictCode, dictCode); + queryWrapper.orderByAsc(GlobalDictItem::getShowOrder); + return globalDictItemMapper.selectList(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java new file mode 100644 index 00000000..1315cd53 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/GlobalDictServiceImpl.java @@ -0,0 +1,190 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.GlobalDictMapper; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictItemService; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 全局字典数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE) +@Service("globalDictService") +public class GlobalDictServiceImpl extends BaseService implements GlobalDictService { + + @Autowired + private GlobalDictMapper globalDictMapper; + @Autowired + private GlobalDictItemService globalDictItemService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return globalDictMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public GlobalDict saveNew(GlobalDict globalDict) { + globalDict.setDictId(idGenerator.nextLongId()); + globalDict.setDeletedFlag(GlobalDeletedFlag.NORMAL); + globalDict.setCreateUserId(TokenData.takeFromRequest().getUserId()); + globalDict.setUpdateUserId(globalDict.getCreateUserId()); + globalDict.setCreateTime(new Date()); + globalDict.setUpdateTime(globalDict.getCreateTime()); + globalDictMapper.insert(globalDict); + return globalDict; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(GlobalDict globalDict, GlobalDict originalGlobalDict) { + this.removeCache(originalGlobalDict.getDictCode()); + globalDict.setCreateUserId(originalGlobalDict.getCreateUserId()); + globalDict.setCreateTime(originalGlobalDict.getCreateTime()); + globalDict.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + globalDict.setUpdateTime(new Date()); + if (globalDictMapper.updateById(globalDict) != 1) { + return false; + } + if (!StrUtil.equals(globalDict.getDictCode(), originalGlobalDict.getDictCode())) { + globalDictItemService.updateNewCode(originalGlobalDict.getDictCode(), globalDict.getDictCode()); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dictId) { + GlobalDict globalDict = this.getById(dictId); + if (globalDict == null) { + return false; + } + this.removeCache(globalDict.getDictCode()); + if (globalDictMapper.deleteById(dictId) == 0) { + return false; + } + GlobalDictItem filter = new GlobalDictItem(); + filter.setDictCode(globalDict.getDictCode()); + globalDictItemService.removeBy(filter); + return true; + } + + @Override + public List getGlobalDictList(GlobalDict filter, String orderBy) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } + return globalDictMapper.selectList(queryWrapper); + } + + @Override + public boolean existDictCode(String dictCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(GlobalDict::getDictCode, dictCode); + return globalDictMapper.selectCount(queryWrapper) > 0; + } + + @Override + public boolean existDictItemFromCache(String dictCode, Serializable itemId) { + return CollUtil.isNotEmpty(this.getGlobalDictItemListFromCache(dictCode, CollUtil.newHashSet(itemId))); + } + + @Override + public List getGlobalDictItemListFromCache(String dictCode, Set itemIds) { + if (CollUtil.isNotEmpty(itemIds) && !(itemIds.iterator().next() instanceof String)) { + itemIds = itemIds.stream().map(Object::toString).collect(Collectors.toSet()); + } + List dataList; + RMap cachedMap = + redissonClient.getMap(RedisKeyUtil.makeGlobalDictKey(dictCode)); + if (cachedMap.isExists()) { + Map dataMap = + CollUtil.isEmpty(itemIds) ? cachedMap.readAllMap() : cachedMap.getAll(itemIds); + dataList = dataMap.values().stream() + .map(c -> JSON.parseObject(c, GlobalDictItem.class)).collect(Collectors.toList()); + dataList.sort(Comparator.comparingInt(GlobalDictItem::getShowOrder)); + } else { + dataList = globalDictItemService.getGlobalDictItemListByDictCode(dictCode); + this.putCache(dictCode, dataList); + if (CollUtil.isNotEmpty(itemIds)) { + Set tmpItemIds = itemIds; + dataList = dataList.stream() + .filter(c -> tmpItemIds.contains(c.getItemId())).collect(Collectors.toList()); + } + } + return dataList; + } + + @Override + public Map getGlobalDictItemDictMapFromCache(String dictCode, Set itemIds) { + List dataList = this.getGlobalDictItemListFromCache(dictCode, itemIds); + return dataList.stream().collect(Collectors.toMap(GlobalDictItem::getItemId, GlobalDictItem::getItemName)); + } + + @Override + public void reloadCachedData(String dictCode) { + this.removeCache(dictCode); + List dataList = globalDictItemService.getGlobalDictItemListByDictCode(dictCode); + this.putCache(dictCode, dataList); + } + + @Override + public void removeCache(String dictCode) { + if (StrUtil.isNotBlank(dictCode)) { + redissonClient.getMap(RedisKeyUtil.makeGlobalDictKey(dictCode)).delete(); + } + } + + private void putCache(String dictCode, List globalDictItemList) { + if (CollUtil.isNotEmpty(globalDictItemList)) { + Map dataMap = globalDictItemList.stream() + .filter(item -> item.getStatus() == GlobalDictItemStatus.NORMAL) + .collect(Collectors.toMap(GlobalDictItem::getItemId, JSON::toJSONString)); + if (MapUtil.isNotEmpty(dataMap)) { + redissonClient.getMap(RedisKeyUtil.makeGlobalDictKey(dictCode)).putAll(dataMap); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java new file mode 100644 index 00000000..623b14b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictItemServiceImpl.java @@ -0,0 +1,190 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.BooleanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.TenantGlobalDictItemMapper; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; +import com.orangeforms.common.dict.service.TenantGlobalDictItemService; +import com.orangeforms.common.dict.service.TenantGlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 租户全局字典项目数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.TENANT_COMMON_DATASOURCE_TYPE) +@Slf4j +@Service("tenantGlobalDictItemService") +public class TenantGlobalDictItemServiceImpl + extends BaseService implements TenantGlobalDictItemService { + + @Autowired + private TenantGlobalDictItemMapper tenantGlobalDictItemMapper; + @Autowired + private TenantGlobalDictService tenantGlobalDictService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return tenantGlobalDictItemMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public TenantGlobalDictItem saveNew(TenantGlobalDict dict, TenantGlobalDictItem dictItem) { + tenantGlobalDictService.removeCache(dict); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + dictItem.setTenantId(TokenData.takeFromRequest().getTenantId()); + } + dictItem.setId(idGenerator.nextLongId()); + dictItem.setDeletedFlag(GlobalDeletedFlag.NORMAL); + dictItem.setStatus(GlobalDictItemStatus.NORMAL); + dictItem.setCreateUserId(TokenData.takeFromRequest().getUserId()); + dictItem.setUpdateUserId(dictItem.getCreateUserId()); + dictItem.setCreateTime(new Date()); + dictItem.setUpdateTime(dictItem.getCreateTime()); + tenantGlobalDictItemMapper.insert(dictItem); + return dictItem; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewBatch(List dictItemList) { + if (CollUtil.isEmpty(dictItemList)) { + return; + } + Date now = new Date(); + for (TenantGlobalDictItem dictItem : dictItemList) { + if (dictItem.getId() == null) { + dictItem.setId(idGenerator.nextLongId()); + } + if (dictItem.getCreateUserId() == null) { + dictItem.setCreateUserId(TokenData.takeFromRequest().getUserId()); + } + dictItem.setUpdateUserId(dictItem.getCreateUserId()); + dictItem.setUpdateTime(now); + dictItem.setCreateTime(now); + dictItem.setStatus(GlobalDictItemStatus.NORMAL); + dictItem.setDeletedFlag(GlobalDeletedFlag.NORMAL); + } + tenantGlobalDictItemMapper.insertList(dictItemList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(TenantGlobalDict dict, TenantGlobalDictItem dictItem, TenantGlobalDictItem originalDictItem) { + tenantGlobalDictService.removeCache(dict); + // 该方法不能直接修改字典状态,更不会修改tenantId。 + dictItem.setStatus(originalDictItem.getStatus()); + dictItem.setTenantId(originalDictItem.getTenantId()); + dictItem.setCreateUserId(originalDictItem.getCreateUserId()); + dictItem.setCreateTime(originalDictItem.getCreateTime()); + dictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + dictItem.setUpdateTime(new Date()); + return tenantGlobalDictItemMapper.updateById(dictItem) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateNewCode(String oldCode, String newCode) { + TenantGlobalDictItem dictItem = new TenantGlobalDictItem(); + dictItem.setDictCode(newCode); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, oldCode); + tenantGlobalDictItemMapper.update(dictItem, queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateStatus(TenantGlobalDict dict, TenantGlobalDictItem dictItem, Integer status) { + tenantGlobalDictService.removeCache(dict); + dictItem.setStatus(status); + dictItem.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + dictItem.setUpdateTime(new Date()); + tenantGlobalDictItemMapper.updateById(dictItem); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(TenantGlobalDict dict, TenantGlobalDictItem dictItem) { + tenantGlobalDictService.removeCache(dict); + return this.removeById(dictItem.getId()); + } + + @Override + public boolean existDictCodeAndItemId(TenantGlobalDict dict, Serializable itemId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dict.getDictCode()); + queryWrapper.eq(TenantGlobalDictItem::getItemId, itemId.toString()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + queryWrapper.eq(TenantGlobalDictItem::getTenantId, TokenData.takeFromRequest().getTenantId()); + } + return tenantGlobalDictItemMapper.selectCount(queryWrapper) > 0; + } + + @Override + public boolean existDictCode(String dictCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dictCode); + return tenantGlobalDictItemMapper.selectCount(queryWrapper) > 0; + } + + @Override + public TenantGlobalDictItem getGlobalDictItemByDictCodeAndItemId(String dictCode, Serializable itemId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dictCode); + queryWrapper.eq(TenantGlobalDictItem::getItemId, itemId.toString()); + return tenantGlobalDictItemMapper.selectOne(queryWrapper); + } + + @Override + public List getGlobalDictItemList(TenantGlobalDictItem filter, String orderBy) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } else { + queryWrapper.orderByAsc(TenantGlobalDictItem::getShowOrder); + } + return tenantGlobalDictItemMapper.selectList(queryWrapper); + } + + @Override + public List getGlobalDictItemList(TenantGlobalDict dict) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDictItem::getDictCode, dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + queryWrapper.eq(TenantGlobalDictItem::getTenantId, TokenData.takeFromRequest().getTenantId()); + } + queryWrapper.orderByAsc(TenantGlobalDictItem::getShowOrder); + return tenantGlobalDictItemMapper.selectList(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java new file mode 100644 index 00000000..d9caab86 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/service/impl/TenantGlobalDictServiceImpl.java @@ -0,0 +1,305 @@ +package com.orangeforms.common.dict.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.dict.constant.GlobalDictItemStatus; +import com.orangeforms.common.dict.dao.TenantGlobalDictMapper; +import com.orangeforms.common.dict.model.TenantGlobalDict; +import com.orangeforms.common.dict.model.TenantGlobalDictItem; +import com.orangeforms.common.dict.service.TenantGlobalDictItemService; +import com.orangeforms.common.dict.service.TenantGlobalDictService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 租户全局字典数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.TENANT_COMMON_DATASOURCE_TYPE) +@Slf4j +@Service("tenantGlobalDictService") +public class TenantGlobalDictServiceImpl + extends BaseService implements TenantGlobalDictService { + + @Autowired + private TenantGlobalDictMapper tenantGlobalDictMapper; + @Autowired + private TenantGlobalDictItemService tenantGlobalDictItemService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return tenantGlobalDictMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public TenantGlobalDict saveNew(TenantGlobalDict dict, Set tenantIdSet) { + String initialData = dict.getInitialData(); + dict.setDictId(idGenerator.nextLongId()); + dict.setDeletedFlag(GlobalDeletedFlag.NORMAL); + dict.setCreateUserId(TokenData.takeFromRequest().getUserId()); + dict.setUpdateUserId(dict.getCreateUserId()); + dict.setCreateTime(new Date()); + dict.setUpdateTime(dict.getCreateTime()); + if (BooleanUtil.isTrue(dict.getTenantCommon())) { + dict.setInitialData(null); + } + tenantGlobalDictMapper.insert(dict); + List dictItemList = null; + if (StrUtil.isNotBlank(initialData)) { + dictItemList = JSONArray.parseArray(initialData, TenantGlobalDictItem.class); + dictItemList.forEach(dictItem -> { + dictItem.setDictCode(dict.getDictCode()); + dictItem.setCreateUserId(dict.getCreateUserId()); + }); + } + if (BooleanUtil.isTrue(dict.getTenantCommon())) { + tenantGlobalDictItemService.saveNewBatch(dictItemList); + } else { + if (CollUtil.isEmpty(tenantIdSet) || dictItemList == null) { + return dict; + } + for (Long tenantId : tenantIdSet) { + dictItemList.forEach(dictItem -> { + dictItem.setId(idGenerator.nextLongId()); + dictItem.setTenantId(tenantId); + }); + tenantGlobalDictItemService.saveNewBatch(dictItemList); + } + } + return dict; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(TenantGlobalDict dict, TenantGlobalDict originalDict) { + this.removeGlobalDictAllCache(originalDict); + dict.setCreateUserId(originalDict.getCreateUserId()); + dict.setCreateTime(originalDict.getCreateTime()); + dict.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + dict.setUpdateTime(new Date()); + if (tenantGlobalDictMapper.updateById(dict) != 1) { + return false; + } + if (!StrUtil.equals(dict.getDictCode(), originalDict.getDictCode())) { + tenantGlobalDictItemService.updateNewCode(originalDict.getDictCode(), dict.getDictCode()); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dictId) { + TenantGlobalDict dict = this.getById(dictId); + if (dict == null) { + return false; + } + this.removeGlobalDictAllCache(dict); + if (tenantGlobalDictMapper.deleteById(dictId) == 0) { + return false; + } + TenantGlobalDictItem filter = new TenantGlobalDictItem(); + filter.setDictCode(dict.getDictCode()); + tenantGlobalDictItemService.removeBy(filter); + return true; + } + + @Override + public List getGlobalDictList(TenantGlobalDict filter, String orderBy) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } + return tenantGlobalDictMapper.selectList(queryWrapper); + } + + @Override + public boolean existDictCode(String dictCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDict::getDictCode, dictCode); + return tenantGlobalDictMapper.selectCount(queryWrapper) > 0; + } + + @Override + public TenantGlobalDict getTenantGlobalDictByDictCode(String dictCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TenantGlobalDict::getDictCode, dictCode); + return tenantGlobalDictMapper.selectOne(queryWrapper); + } + + @Override + public TenantGlobalDict getTenantGlobalDictFromCache(String dictCode) { + String key = RedisKeyUtil.makeGlobalDictOnlyKey(dictCode); + RBucket bucket = redissonClient.getBucket(key); + if (bucket.isExists()) { + return JSON.parseObject(bucket.get(), TenantGlobalDict.class); + } + TenantGlobalDict dict = this.getTenantGlobalDictByDictCode(dictCode); + if (dict != null) { + bucket.set(JSON.toJSONString(dict)); + } + return dict; + } + + @Override + public List getGlobalDictItemListFromCache(TenantGlobalDict dict, Set itemIds) { + if (CollUtil.isNotEmpty(itemIds) && !(itemIds.iterator().next() instanceof String)) { + itemIds = itemIds.stream().map(Object::toString).collect(Collectors.toSet()); + } + String key = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + key = this.appendTenantSuffix(key); + } + List dataList; + RMap cachedMap = redissonClient.getMap(key); + if (cachedMap.isExists()) { + Map dataMap = + CollUtil.isEmpty(itemIds) ? cachedMap.readAllMap() : cachedMap.getAll(itemIds); + dataList = dataMap.values().stream() + .map(c -> JSON.parseObject(c, TenantGlobalDictItem.class)).collect(Collectors.toList()); + dataList.sort(Comparator.comparingInt(TenantGlobalDictItem::getShowOrder)); + } else { + dataList = tenantGlobalDictItemService.getGlobalDictItemList(dict); + this.putCache(dict, dataList); + if (CollUtil.isNotEmpty(itemIds)) { + Set tmpItemIds = itemIds; + dataList = dataList.stream() + .filter(c -> tmpItemIds.contains(c.getItemId())).collect(Collectors.toList()); + } + } + return dataList; + } + + @Override + public Map getGlobalDictItemDictMapFromCache( + TenantGlobalDict dict, Set itemIds) { + List dataList = this.getGlobalDictItemListFromCache(dict, itemIds); + return dataList.stream() + .collect(Collectors.toMap(TenantGlobalDictItem::getItemId, TenantGlobalDictItem::getItemName)); + } + + @Override + public void reloadCachedData(TenantGlobalDict dict) { + this.removeCache(dict); + List dataList = tenantGlobalDictItemService.getGlobalDictItemList(dict); + this.putCache(dict, dataList); + } + + @Override + public void reloadAllTenantCachedData(TenantGlobalDict dict) { + if (StrUtil.isBlank(dict.getDictCode())) { + return; + } + String dictCodeKey = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + redissonClient.getKeys().deleteByPattern(dictCodeKey + "*"); + TenantGlobalDictItem filter = new TenantGlobalDictItem(); + filter.setDictCode(dict.getDictCode()); + List dictItemList = + tenantGlobalDictItemService.getGlobalDictItemList(filter, null); + if (CollUtil.isEmpty(dictItemList)) { + return; + } + Map> dictItemMap = + dictItemList.stream().collect(Collectors.groupingBy(TenantGlobalDictItem::getTenantId)); + for (Map.Entry> entry : dictItemMap.entrySet()) { + String key = dictCodeKey + "-" + entry.getKey(); + Map dataMap = entry.getValue().stream() + .collect(Collectors.toMap(TenantGlobalDictItem::getItemId, JSON::toJSONString)); + RMap cachedMap = redissonClient.getMap(key); + cachedMap.putAll(dataMap); + cachedMap.expire(1, TimeUnit.DAYS); + } + } + + @Override + public void removeCache(TenantGlobalDict dict) { + if (StrUtil.isBlank(dict.getDictCode())) { + return; + } + String key = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + key = this.appendTenantSuffix(key); + } + redissonClient.getMap(key).delete(); + } + + @Override + public boolean existDictItemFromCache(String dictCode, Serializable itemId) { + TenantGlobalDict tenantGlobalDict = this.getTenantGlobalDictFromCache(dictCode); + return CollUtil.isNotEmpty(this.getGlobalDictItemListFromCache(tenantGlobalDict, CollUtil.newHashSet(itemId))); + } + + private void putCache(TenantGlobalDict dict, List dictItemList) { + if (CollUtil.isEmpty(dictItemList)) { + return; + } + String key = RedisKeyUtil.makeGlobalDictKey(dict.getDictCode()); + if (BooleanUtil.isFalse(dict.getTenantCommon())) { + key = this.appendTenantSuffix(key); + } + Map dataMap = dictItemList.stream() + .filter(item -> item.getStatus() == GlobalDictItemStatus.NORMAL) + .collect(Collectors.toMap(TenantGlobalDictItem::getItemId, JSON::toJSONString)); + if (MapUtil.isNotEmpty(dataMap)) { + RMap cachedMap = redissonClient.getMap(key); + cachedMap.putAll(dataMap); + cachedMap.expire(1, TimeUnit.DAYS); + } + } + + private String appendTenantSuffix(String key) { + return key + "-" + TokenData.takeFromRequest().getTenantId(); + } + + private void removeGlobalDictAllCache(TenantGlobalDict dict) { + String dictCode = dict.getDictCode(); + if (StrUtil.isBlank(dictCode)) { + return; + } + String key = RedisKeyUtil.makeGlobalDictOnlyKey(dictCode); + redissonClient.getBucket(key).delete(); + key = RedisKeyUtil.makeGlobalDictKey(dictCode); + if (BooleanUtil.isTrue(dict.getTenantCommon())) { + redissonClient.getMap(key).delete(); + } else { + redissonClient.getKeys().deleteByPatternAsync(key + "*"); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java new file mode 100644 index 00000000..05e308ef --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/util/GlobalDictOperationHelper.java @@ -0,0 +1,89 @@ +package com.orangeforms.common.dict.util; + +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.dict.dto.GlobalDictDto; +import com.orangeforms.common.dict.model.GlobalDict; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.dict.vo.GlobalDictVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 全局编码字典操作的通用帮助对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class GlobalDictOperationHelper { + + @Autowired + private GlobalDictService globalDictService; + + /** + * 获取全部编码字典列表。 + * + * @param globalDictDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 字典的数据列表。 + */ + public ResponseResult> listAllGlobalDict( + GlobalDictDto globalDictDtoFilter, MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + GlobalDict filter = MyModelUtil.copyTo(globalDictDtoFilter, GlobalDict.class); + List dictList = globalDictService.getGlobalDictList(filter, null); + List dictVoList = MyModelUtil.copyCollectionTo(dictList, GlobalDictVo.class); + long totalCount = 0L; + if (dictList instanceof Page) { + totalCount = ((Page) dictList).getTotal(); + } + return ResponseResult.success(MyPageUtil.makeResponseData(dictVoList, totalCount)); + } + + public List> toDictDataList(List resultList, String itemIdType) { + return resultList.stream().map(item -> { + Map dataMap = new HashMap<>(4); + Object itemId = item.getItemId(); + if (StrUtil.equals(itemIdType, "Long")) { + itemId = Long.valueOf(item.getItemId()); + } else if (StrUtil.equals(itemIdType, "Integer")) { + itemId = Integer.valueOf(item.getItemId()); + } + dataMap.put(ApplicationConstant.DICT_ID, itemId); + dataMap.put(ApplicationConstant.DICT_NAME, item.getItemName()); + dataMap.put("showOrder", item.getShowOrder()); + dataMap.put("status", item.getStatus()); + return dataMap; + }).collect(Collectors.toList()); + } + + public List> toDictDataList2(List resultList) { + return resultList.stream().map(item -> { + Map dataMap = new HashMap<>(5); + dataMap.put(ApplicationConstant.DICT_ID, item.getId()); + dataMap.put("itemId", item.getItemId()); + dataMap.put(ApplicationConstant.DICT_NAME, item.getItemName()); + dataMap.put("showOrder", item.getShowOrder()); + dataMap.put("status", item.getStatus()); + return dataMap; + }).collect(Collectors.toList()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java new file mode 100644 index 00000000..cbf07bd4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictItemVo.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典项目Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典项目Vo") +@Data +public class GlobalDictItemVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long id; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + private String dictCode; + + /** + * 字典数据项Id。 + */ + @Schema(description = "字典数据项Id") + private String itemId; + + /** + * 字典数据项名称。 + */ + @Schema(description = "字典数据项名称") + private String itemName; + + /** + * 显示顺序(数值越小越靠前)。 + */ + @Schema(description = "显示顺序") + private Integer showOrder; + + /** + * 字典状态。具体值引用DictItemStatus常量类。 + */ + @Schema(description = "字典状态") + private Integer status; + + /** + * 创建用户Id。 + */ + @Schema(description = "创建用户Id") + private Long createUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建用户名。 + */ + @Schema(description = "创建用户名") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java new file mode 100644 index 00000000..f77a2581 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/GlobalDictVo.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 全局系统字典Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "全局系统字典Vo") +@Data +public class GlobalDictVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long dictId; + + /** + * 字典编码。 + */ + @Schema(description = "字典编码") + private String dictCode; + + /** + * 字典中文名称。 + */ + @Schema(description = "字典中文名称") + private String dictName; + + /** + * 创建用户Id。 + */ + @Schema(description = "创建用户Id") + private Long createUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建用户名。 + */ + @Schema(description = "创建用户名") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java new file mode 100644 index 00000000..967b561d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictItemVo.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典项目Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典项目Vo") +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantGlobalDictItemVo extends GlobalDictItemVo { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java new file mode 100644 index 00000000..94ac38fc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-dict/src/main/java/com/orangeforms/common/dict/vo/TenantGlobalDictVo.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户全局系统字典Vo。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "租户全局系统字典Vo") +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantGlobalDictVo extends GlobalDictVo { + + /** + * 是否为所有租户的通用字典。 + */ + @Schema(description = "是否为所有租户的通用字典") + private Boolean tenantCommon; + + /** + * 租户的非公用字典的初始化字典数据。 + */ + @Schema(description = "租户的非公用字典的初始化字典数据") + private String initialData; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-ext/pom.xml new file mode 100644 index 00000000..f34963db --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/pom.xml @@ -0,0 +1,21 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-ext + + + + com.orangeforms + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java new file mode 100644 index 00000000..81673674 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/base/BizWidgetDatasource.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.ext.base; + +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; + +import java.util.List; +import java.util.Map; + +/** + * 业务组件获取数据的数据源接口。 + * 如果业务服务集成了common-ext组件,可以通过实现该接口的方式,为BizWidgetController访问提供数据。 + * 对于没有集成common-ext组件的服务,可以通过http方式,为BizWidgetController访问提供数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface BizWidgetDatasource { + + /** + * 获取指定通用业务组件的数据。 + * + * @param widgetType 业务组件类型。 + * @param filter 过滤参数。不同的数据源参数不同。这里我们以键值对的方式传递。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 查询后的分页数据列表。 + */ + MyPageData> getDataList( + String widgetType, Map filter, MyOrderParam orderParam, MyPageParam pageParam); + + /** + * 获取指定主键Id的数据对象。 + * + * @param widgetType 业务组件类型。 + * @param fieldName 字段名,如果为空,则使用主键字段名。 + * @param fieldValues 字段值集合。 + * @return 指定主键Id的数据对象。 + */ + List> getDataListWithInList(String widgetType, String fieldName, List fieldValues); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java new file mode 100644 index 00000000..41180d8c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.ext.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-ext通用扩展模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({CommonExtProperties.class}) +public class CommonExtAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java new file mode 100644 index 00000000..7aeb2c23 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/config/CommonExtProperties.java @@ -0,0 +1,76 @@ +package com.orangeforms.common.ext.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.Data; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * common-ext配置属性类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-ext") +public class CommonExtProperties implements InitializingBean { + + /** + * 上传存储类型。具体值可参考枚举 UploadStoreTypeEnum。默认0为本地存储。 + */ + @Value("${common-ext.uploadStoreType:0}") + private Integer uploadStoreType; + + /** + * 仅当uploadStoreType等于0的时候,该配置值生效。 + */ + @Value("${common-ext.uploadFileBaseDir:./zz-resource/upload-files/commonext}") + private String uploadFileBaseDir; + + private List apps; + + private Map applicationMap; + + @Override + public void afterPropertiesSet() throws Exception { + if (CollUtil.isEmpty(apps)) { + applicationMap = new HashMap<>(1); + } else { + applicationMap = apps.stream().collect(Collectors.toMap(AppProperties::getAppCode, c -> c)); + } + } + + @Data + public static class AppProperties { + /** + * 应用编码。 + */ + private String appCode; + /** + * 通用业务组件数据源属性列表。 + */ + private List bizWidgetDatasources; + } + + @Data + public static class BizWidgetDatasourceProperties { + /** + * 通用业务组件的数据源类型。多个类型之间逗号分隔,如:upms_user,upms_dept。 + */ + private String types; + /** + * 列表数据接口地址。格式为完整的url,如:http://xxxxx + */ + private String listUrl; + /** + * 详情数据接口地址。格式为完整的url,如:http://xxxxx + */ + private String viewUrl; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java new file mode 100644 index 00000000..5d3b4ae6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/constant/BizWidgetDatasourceType.java @@ -0,0 +1,41 @@ +package com.orangeforms.common.ext.constant; + +/** + * 业务组件数据源类型常量类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class BizWidgetDatasourceType { + + /** + * 通用用户组件数据源类型。 + */ + public static final String UPMS_USER_TYPE = "upms_user"; + + /** + * 通用部门组件数据源类型。 + */ + public static final String UPMS_DEPT_TYPE = "upms_dept"; + + /** + * 通用角色组件数据源类型。 + */ + public static final String UPMS_ROLE_TYPE = "upms_role"; + + /** + * 通用岗位组件数据源类型。 + */ + public static final String UPMS_POST_TYPE = "upms_post"; + + /** + * 通用部门岗位组件数据源类型。 + */ + public static final String UPMS_DEPT_POST_TYPE = "upms_dept_post"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private BizWidgetDatasourceType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java new file mode 100644 index 00000000..021ac5e1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/BizWidgetController.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.ext.controller; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.ext.util.BizWidgetDatasourceExtHelper; +import com.orangeforms.common.core.annotation.MyRequestBody; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 业务组件获取数据的访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@RestController +@RequestMapping("${common-ext.urlPrefix}/bizwidget") +public class BizWidgetController { + + @Autowired + private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper; + + @PostMapping("/list") + public ResponseResult>> list( + @MyRequestBody(required = true) String widgetType, + @MyRequestBody JSONObject filter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + String appCode = TokenData.takeFromRequest().getAppCode(); + MyPageData> pageData = + bizWidgetDatasourceExtHelper.getDataList(appCode, widgetType, filter, orderParam, pageParam); + return ResponseResult.success(pageData); + } + + /** + * 查看指定多条数据的详情。 + * + * @param widgetType 组件类型。 + * @param fieldName 字段名,如果为空则默认为主键过滤。 + * @param fieldValues 字段值。多个值之间逗号分割。 + * @return 详情数据。 + */ + @PostMapping("/view") + public ResponseResult>> view( + @MyRequestBody(required = true) String widgetType, + @MyRequestBody String fieldName, + @MyRequestBody(required = true) String fieldValues) { + String appCode = TokenData.takeFromRequest().getAppCode(); + List> dataMapList = + bizWidgetDatasourceExtHelper.getDataListWithInList(appCode, widgetType, fieldName, fieldValues); + return ResponseResult.success(dataMapList); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java new file mode 100644 index 00000000..0d94cc1c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/controller/UtilController.java @@ -0,0 +1,112 @@ +package com.orangeforms.common.ext.controller; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.ext.config.CommonExtProperties; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBinaryStream; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; + +/** + * 扩展工具接口类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@RestController +@RequestMapping("${common-ext.urlPrefix}/util") +public class UtilController { + + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private CommonExtProperties properties; + @Autowired + private RedissonClient redissonClient; + + private static final String IMAGE_DATA_FIELD = "imageData"; + + /** + * 上传图片数据。 + * + * @param uploadFile 上传图片文件。 + */ + @PostMapping("/uploadImage") + public void uploadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + BaseUpDownloader upDownloader = + upDownloaderFactory.get(EnumUtil.getEnumAt(UploadStoreTypeEnum.class, properties.getUploadStoreType())); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + properties.getUploadFileBaseDir(), "CommonExt", IMAGE_DATA_FIELD, true, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + String uploadUri = ContextUtil.getHttpRequest().getRequestURI(); + uploadUri = StrUtil.removeSuffix(uploadUri, "/"); + uploadUri = StrUtil.removeSuffix(uploadUri, "/uploadImage"); + responseInfo.setDownloadUri(uploadUri + "/downloadImage"); + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + /** + * 下载图片数据。 + * + * @param filename 文件名。 + * @param response Http 应答对象。 + */ + @GetMapping("/downloadImage") + public void downloadImage(@RequestParam String filename, HttpServletResponse response) { + try { + BaseUpDownloader upDownloader = + upDownloaderFactory.get(EnumUtil.getEnumAt(UploadStoreTypeEnum.class, properties.getUploadStoreType())); + upDownloader.doDownload(properties.getUploadFileBaseDir(), + "CommonExt", IMAGE_DATA_FIELD, filename, true, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + /** + * 下载缓存的会话图片数据。 + * + * @param filename 文件名。 + * @param response Http 应答对象。 + */ + @GetMapping("/downloadSessionImage") + public void downloadSessionImage(@RequestParam String filename, HttpServletResponse response) throws IOException { + TokenData tokenData = TokenData.takeFromRequest(); + String key = tokenData.getSessionId() + filename; + RBinaryStream stream = redissonClient.getBinaryStream(key); + if (!stream.isExists()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "无效的会话缓存图片!")); + } + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + filename); + try (OutputStream os = response.getOutputStream()) { + os.write(stream.getAndDelete()); + } catch (IOException e) { + log.error("Failed to call LocalUpDownloader.doDownload", e); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java new file mode 100644 index 00000000..ba9cef17 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/java/com/orangeforms/common/ext/util/BizWidgetDatasourceExtHelper.java @@ -0,0 +1,209 @@ +package com.orangeforms.common.ext.util; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.ext.base.BizWidgetDatasource; +import com.orangeforms.common.ext.config.CommonExtProperties; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 高级通用业务组件的扩展帮助实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class BizWidgetDatasourceExtHelper { + + @Autowired + private CommonExtProperties properties; + /** + * 全部框架使用橙单框架,同时组件所在模块,如在线表单,报表等和业务服务位于同一服务内是使用。 + */ + private static final String DEFAULT_ORANGE_APP = "__DEFAULT_ORANGE_APP__"; + /** + * Map的数据结构为:Map> + */ + private Map> dataExtractorMap = MapUtil.newHashMap(); + + @PostConstruct + private void laodThirdPartyAppConfig() { + Map appPropertiesMap = properties.getApplicationMap(); + if (MapUtil.isEmpty(appPropertiesMap)) { + return; + } + for (Map.Entry entry : appPropertiesMap.entrySet()) { + String appCode = entry.getKey(); + List datasources = entry.getValue().getBizWidgetDatasources(); + Map m = new HashMap<>(datasources.size()); + for (CommonExtProperties.BizWidgetDatasourceProperties datasource : datasources) { + List types = StrUtil.split(datasource.getTypes(), ","); + DatasourceWrapper w = new DatasourceWrapper(); + w.setListUrl(datasource.getListUrl()); + w.setViewUrl(datasource.getViewUrl()); + for (String type : types) { + m.put(type, w); + } + } + dataExtractorMap.put(appCode, m); + } + } + + /** + * 为默认APP注册基础组件数据源对象。 + * + * @param type 数据源类型。 + * @param datasource 业务通用组件的数据源接口。 + */ + public void registerDatasource(String type, BizWidgetDatasource datasource) { + Assert.notBlank(type); + Assert.notNull(datasource); + Map datasourceWrapperMap = + dataExtractorMap.computeIfAbsent(DEFAULT_ORANGE_APP, k -> new HashMap<>(2)); + datasourceWrapperMap.put(type, new DatasourceWrapper(datasource)); + } + + /** + * 根据过滤条件获取指定通用业务组件的数据列表。 + * + * @param appCode 接入应用编码。如果为空,则使用默认的 DEFAULT_ORANGE_APP。 + * @param type 组件数据源类型。 + * @param filter 过滤参数。不同的数据源参数不同。这里我们以键值对的方式传递。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 查询后的分页数据列表。 + */ + public MyPageData> getDataList( + String appCode, String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + if (StrUtil.isBlank(type)) { + throw new MyRuntimeException("Argument [types] can't be BLANK"); + } + if (StrUtil.isBlank(appCode)) { + return this.getDataList(type, filter, orderParam, pageParam); + } + DatasourceWrapper wrapper = this.getDatasourceWrapper(appCode, type); + JSONObject body = new JSONObject(); + body.put("type", type); + if (MapUtil.isNotEmpty(filter)) { + body.put("filter", filter); + } + if (orderParam != null) { + body.put("orderParam", orderParam); + } + if (pageParam != null) { + body.put("pageParam", pageParam); + } + String response = this.invokeThirdPartyUrlWithPost(wrapper.getListUrl(), body.toJSONString()); + ResponseResult>> responseResult = + JSON.parseObject(response, new TypeReference>>>() { + }); + if (!responseResult.isSuccess()) { + throw new MyRuntimeException(responseResult.getErrorMessage()); + } + return responseResult.getData(); + } + + /** + * 根据指定字段的集合获取指定通用业务组件的数据对象列表。 + * + * @param appCode 接入应用Id。如果为空,则使用默认的 DEFAULT_ORANGE_APP。 + * @param type 组件数据源类型。 + * @param fieldName 字段名称。 + * @param fieldValues 字段值结合。 + * @return 指定字段数据集合的数据对象列表。 + */ + public List> getDataListWithInList( + String appCode, String type, String fieldName, String fieldValues) { + if (StrUtil.isBlank(fieldValues)) { + throw new MyRuntimeException("Argument [fieldValues] can't be BLANK"); + } + if (StrUtil.isBlank(type)) { + throw new MyRuntimeException("Argument [types] can't be BLANK"); + } + if (StrUtil.isBlank(appCode)) { + return this.getDataListWithInList(type, fieldName, fieldValues); + } + DatasourceWrapper wrapper = this.getDatasourceWrapper(appCode, type); + JSONObject body = new JSONObject(); + body.put("type", type); + if (StrUtil.isNotBlank(fieldName)) { + body.put("fieldName", fieldName); + } + body.put("fieldValues", fieldValues); + String response = this.invokeThirdPartyUrlWithPost(wrapper.getViewUrl(), body.toJSONString()); + ResponseResult>> responseResult = + JSON.parseObject(response, new TypeReference>>>() { + }); + if (!responseResult.isSuccess()) { + throw new MyRuntimeException(responseResult.getErrorMessage()); + } + return responseResult.getData(); + } + + private MyPageData> getDataList( + String type, Map filter, MyOrderParam orderParam, MyPageParam pageParam) { + DatasourceWrapper wrapper = this.getDatasourceWrapper(DEFAULT_ORANGE_APP, type); + return wrapper.getBizWidgetDataSource().getDataList(type, filter, orderParam, pageParam); + } + + private List> getDataListWithInList(String type, String fieldName, String fieldValues) { + DatasourceWrapper wrapper = this.getDatasourceWrapper(DEFAULT_ORANGE_APP, type); + return wrapper.getBizWidgetDataSource().getDataListWithInList(type, fieldName, StrUtil.split(fieldValues, ",")); + } + + private String invokeThirdPartyUrlWithPost(String url, String body) { + String token = TokenData.takeFromRequest().getToken(); + Map headerMap = new HashMap<>(1); + headerMap.put("Authorization", token); + StringBuilder fullUrl = new StringBuilder(128); + fullUrl.append(url).append("?token=").append(token); + HttpResponse httpResponse = HttpUtil.createPost(fullUrl.toString()).body(body).addHeaders(headerMap).execute(); + if (!httpResponse.isOk()) { + String msg = StrFormatter.format( + "Failed to call [{}] with ERROR HTTP Status [{}] and [{}].", + url, httpResponse.getStatus(), httpResponse.body()); + log.error(msg); + throw new MyRuntimeException(msg); + } + return httpResponse.body(); + } + + private DatasourceWrapper getDatasourceWrapper(String appCode, String type) { + Map datasourceWrapperMap = dataExtractorMap.get(appCode); + Assert.notNull(datasourceWrapperMap); + DatasourceWrapper wrapper = datasourceWrapperMap.get(type); + Assert.notNull(wrapper); + return wrapper; + } + + @NoArgsConstructor + @Data + public static class DatasourceWrapper { + private BizWidgetDatasource bizWidgetDataSource; + private String listUrl; + private String viewUrl; + + public DatasourceWrapper(BizWidgetDatasource bizWidgetDataSource) { + this.bizWidgetDataSource = bizWidgetDataSource; + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..fc140409 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-ext/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.ext.config.CommonExtAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/pom.xml new file mode 100644 index 00000000..9e40544e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-flow-online + 1.0.0 + common-flow-online + jar + + + + com.orangeforms + common-flow + 1.0.0 + + + com.orangeforms + common-online + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java new file mode 100644 index 00000000..07538229 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.online.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-flow-online模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({FlowOnlineProperties.class}) +public class FlowOnlineAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java new file mode 100644 index 00000000..143afba4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/config/FlowOnlineProperties.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.flow.online.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 在线表单工作流模块的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-flow-online") +public class FlowOnlineProperties { + + /** + * 在线表单的URL前缀。 + */ + private String urlPrefix; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java new file mode 100644 index 00000000..94dbf0c1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/controller/FlowOnlineOperationController.java @@ -0,0 +1,1089 @@ +package com.orangeforms.common.flow.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.dto.FlowTaskCommentDto; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowMessageType; +import com.orangeforms.common.flow.online.service.FlowOnlineOperationService; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowMessageService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.util.FlowOperationHelper; +import com.orangeforms.common.flow.vo.FlowEntryVo; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.util.OnlineOperationHelper; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 工作流在线表单流程操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流在线表单流程操作接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowOnlineOperation") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowOnlineOperationController { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowOperationHelper flowOperationHelper; + @Autowired + private FlowOnlineOperationService flowOnlineOperationService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowMessageService flowMessageService; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineOperationHelper onlineOperationHelper; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private SessionCacheHelper sessionCacheHelper; + + private static final String ONE_TO_MANY_VAR_SUFFIX = "List"; + + /** + * 根据指定流程的主版本,发起一个流程实例,同时作为第一个任务节点的执行人,执行第一个用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * 注:流程设计页面的"启动"按钮,调用该接口可以启动任何流程用于流程配置后的测试验证。 + * + * @param processDefinitionKey 流程定义标识。 + * @param flowTaskCommentDto 审批意见。 + * @param taskVariableData 流程任务变量数据。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @param copyData 传阅数据,格式为type和id,type的值参考FlowConstant中的常量值。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.START_FLOW) + @PostMapping("/startPreview") + public ResponseResult startPreview( + @MyRequestBody(required = true) String processDefinitionKey, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto, + @MyRequestBody JSONObject taskVariableData, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData, + @MyRequestBody JSONObject copyData) { + return this.startAndTake( + processDefinitionKey, flowTaskCommentDto, taskVariableData, masterData, slaveData, copyData); + } + + /** + * 根据指定流程的主版本,发起一个流程实例,同时作为第一个任务节点的执行人,执行第一个用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processDefinitionKey 流程定义标识。 + * @param flowTaskCommentDto 审批意见。 + * @param taskVariableData 流程任务变量数据。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @param copyData 传阅数据,格式为type和id,type的值参考FlowConstant中的常量值。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.START_FLOW) + @PostMapping("/startAndTakeUserTask/{processDefinitionKey}") + public ResponseResult startAndTakeUserTask( + @PathVariable("processDefinitionKey") String processDefinitionKey, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto, + @MyRequestBody JSONObject taskVariableData, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData, + @MyRequestBody JSONObject copyData) { + return this.startAndTake( + processDefinitionKey, flowTaskCommentDto, taskVariableData, masterData, slaveData, copyData); + } + + /** + * 启动流程并创建工单,同时将当前录入的数据存入草稿。 + * + * @param processDefinitionKey 流程定义标识。 + * @param processInstanceId 流程实例Id。第一次保存时,该值为null。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @return 应答结果对象,草稿的待办任务对象。 + */ + @DisableDataFilter + @SaTokenDenyAuth + @PostMapping("/startAndSaveDraft/{processDefinitionKey}") + public ResponseResult startAndSaveDraft( + @PathVariable("processDefinitionKey") String processDefinitionKey, + @MyRequestBody String processInstanceId, + @MyRequestBody JSONObject masterData, + @MyRequestBody JSONObject slaveData) { + String errorMessage; + if (MapUtil.isEmpty(masterData) && MapUtil.isEmpty(slaveData)) { + errorMessage = "数据验证失败,业务数据不能全部为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult> verifyResult = + this.verifyAndGetFlowEntryPublishAndDatasource(processDefinitionKey, true); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntryPublish flowEntryPublish = verifyResult.getData().getFirst(); + OnlineTable masterTable = verifyResult.getData().getSecond().getMasterTable(); + // 自动填充创建人数据。 + for (OnlineColumn column : masterTable.getColumnMap().values()) { + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.CREATE_USER_ID)) { + masterData.put(column.getColumnName(), TokenData.takeFromRequest().getUserId()); + } else if (ObjectUtil.equals(column.getFieldKind(), FieldKind.CREATE_DEPT_ID)) { + masterData.put(column.getColumnName(), TokenData.takeFromRequest().getDeptId()); + } + } + FlowWorkOrder flowWorkOrder; + if (processInstanceId == null) { + flowWorkOrder = flowOnlineOperationService.saveNewDraftAndStartProcess( + flowEntryPublish.getProcessDefinitionId(), masterTable.getTableId(), masterData, slaveData); + } else { + ResponseResult flowWorkOrderResult = + flowOperationHelper.verifyAndGetFlowWorkOrderWithDraft(processDefinitionKey, processInstanceId); + if (!flowWorkOrderResult.isSuccess()) { + return ResponseResult.errorFrom(flowWorkOrderResult); + } + flowWorkOrder = flowWorkOrderResult.getData(); + flowWorkOrderService.updateDraft(flowWorkOrderResult.getData().getWorkOrderId(), + JSON.toJSONString(masterData), JSON.toJSONString(slaveData)); + } + List taskList = flowApiService.getProcessInstanceActiveTaskList(flowWorkOrder.getProcessInstanceId()); + List flowTaskVoList = flowApiService.convertToFlowTaskList(taskList); + return ResponseResult.success(flowTaskVoList.get(0)); + } + + /** + * 提交流程的用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskCommentDto 流程审批数据。 + * @param taskVariableData 流程任务变量数据。 + * @param masterData 流程审批相关的主表数据。 + * @param slaveData 流程审批相关的多个从表数据。 + * @param copyData 传阅数据,格式为type和id,type的值参考FlowConstant中的常量值。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.SUBMIT_TASK) + @PostMapping("/submitUserTask") + public ResponseResult submitUserTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto, + @MyRequestBody JSONObject taskVariableData, + @MyRequestBody JSONObject masterData, + @MyRequestBody JSONObject slaveData, + @MyRequestBody JSONObject copyData) { + String errorMessage; + // 验证流程任务的合法性。 + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + CallResult assigneeVerifyResult = flowApiService.verifyAssigneeOrCandidateAndClaim(task); + if (!assigneeVerifyResult.isSuccess()) { + return ResponseResult.errorFrom(assigneeVerifyResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + String dataId = instance.getBusinessKey(); + // 这里把传阅数据放到任务变量中,是为了避免给流程数据操作方法增加额外的方法调用参数。 + if (MapUtil.isNotEmpty(copyData)) { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.COPY_DATA_KEY, copyData); + } + FlowTaskComment flowTaskComment = BeanUtil.copyProperties(flowTaskCommentDto, FlowTaskComment.class); + if (StrUtil.isBlank(dataId)) { + return this.submitNewTask(processInstanceId, taskId, + flowTaskComment, taskVariableData, datasource, masterData, slaveData); + } + try { + if (StrUtil.equals(flowTaskComment.getApprovalType(), FlowApprovalType.TRANSFER) + && StrUtil.isBlank(flowTaskComment.getDelegateAssignee())) { + errorMessage = "数据验证失败,加签或转办任务指派人不能为空!!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + flowOnlineOperationService.updateAndTakeTask( + task, flowTaskComment, taskVariableData, datasource, masterData, dataId, slaveDataListResult.getData()); + } catch (FlowOperationException e) { + log.error("Failed to call [FlowOnlineOperationService.updateAndTakeTask]", e); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, e.getMessage()); + } + return ResponseResult.success(); + } + + /** + * 查看指定流程实例的草稿数据。 + * NOTE: 白名单接口。 + * + * @param processDefinitionKey 流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @return 流程实例的草稿数据。 + */ + @DisableDataFilter + @GetMapping("/viewDraftData") + public ResponseResult viewDraftData( + @RequestParam String processDefinitionKey, @RequestParam String processInstanceId) { + String errorMessage; + ResponseResult flowWorkOrderResult = + flowOperationHelper.verifyAndGetFlowWorkOrderWithDraft(processDefinitionKey, processInstanceId); + if (!flowWorkOrderResult.isSuccess()) { + return ResponseResult.errorFrom(flowWorkOrderResult); + } + FlowWorkOrder flowWorkOrder = flowWorkOrderResult.getData(); + if (flowWorkOrder.getOnlineTableId() == null) { + errorMessage = "数据验证失败,当前工单不是在线表单工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowWorkOrderExt flowWorkOrderExt = + flowWorkOrderService.getFlowWorkOrderExtByWorkOrderId(flowWorkOrder.getWorkOrderId()); + if (StrUtil.isBlank(flowWorkOrderExt.getDraftData())) { + return ResponseResult.success(null); + } + Long tableId = flowWorkOrder.getOnlineTableId(); + OnlineTable masterTable = onlineTableService.getOnlineTableFromCache(tableId); + JSONObject draftData = JSON.parseObject(flowWorkOrderExt.getDraftData()); + JSONObject masterData = draftData.getJSONObject(FlowConstant.MASTER_DATA_KEY); + JSONObject slaveData = draftData.getJSONObject(FlowConstant.SLAVE_DATA_KEY); + OnlineDatasource datasource = + onlineDatasourceService.getOnlineDatasourceByMasterTableId(tableId); + List slaveRelationList = null; + if (slaveData != null) { + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + slaveRelationList = relationListResult.getData(); + } + datasource.setMasterTable(masterTable); + JSONObject jsonData = this.buildDraftData(datasource, masterData, slaveRelationList, slaveData); + return ResponseResult.success(jsonData); + } + + /** + * 获取当前流程实例的详情数据。包括主表数据、一对一从表数据、一对多从表数据列表等。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processInstanceId 当前运行时的流程实例Id。 + * @param taskId 流程任务Id。 + * @return 当前流程实例的详情数据。 + */ + @DisableDataFilter + @GetMapping("/viewUserTask") + public ResponseResult viewUserTask( + @RequestParam String processInstanceId, @RequestParam String taskId) { + // 验证流程任务的合法性。 + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + // 如果业务主数据为空,则直接返回。 + if (StrUtil.isBlank(instance.getBusinessKey())) { + return ResponseResult.success(null); + } + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceResult.getData().getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + JSONObject jsonData = this.buildUserTaskData( + instance.getBusinessKey(), datasourceResult.getData(), relationListResult.getData()); + return ResponseResult.success(jsonData); + } + + /** + * 获取已经结束的流程实例的详情数据。包括主表数据、一对一从表数据、一对多从表数据列表等。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processInstanceId 历史流程实例Id。 + * @param taskId 历史任务Id。如果该值为null,仅有发起人可以查看当前流程数据,否则只有任务的指派人才能查看。 + * @return 历史流程实例的详情数据。 + */ + @DisableDataFilter + @GetMapping("/viewHistoricProcessInstance") + public ResponseResult viewHistoricProcessInstance( + @RequestParam String processInstanceId, @RequestParam(required = false) String taskId) { + // 验证流程实例的合法性。 + ResponseResult verifyResult = + flowOperationHelper.verifyAndGetHistoricProcessInstance(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + HistoricProcessInstance instance = verifyResult.getData(); + if (StrUtil.isBlank(instance.getBusinessKey())) { + // 对于没有提交过任何用户任务的场景,可直接返回空数据。 + return ResponseResult.success(new JSONObject()); + } + FlowEntryPublish flowEntryPublish = + flowEntryService.getFlowEntryPublishList(CollUtil.newHashSet(instance.getProcessDefinitionId())).get(0); + TaskInfoVo taskInfoVo = JSON.parseObject(flowEntryPublish.getInitTaskInfo(), TaskInfoVo.class); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfoVo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceResult.getData().getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + JSONObject jsonData = this.buildUserTaskData( + instance.getBusinessKey(), datasourceResult.getData(), relationListResult.getData()); + return ResponseResult.success(jsonData); + } + + /** + * 根据消息Id,获取流程Id关联的业务数据。 + * NOTE:白名单接口。 + * + * @param messageId 抄送消息Id。 + * @return 抄送消息关联的流程实例业务数据。 + */ + @DisableDataFilter + @GetMapping("/viewCopyBusinessData") + public ResponseResult viewCopyBusinessData(@RequestParam Long messageId) { + String errorMessage; + // 验证流程任务的合法性。 + FlowMessage flowMessage = flowMessageService.getById(messageId); + if (flowMessage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (flowMessage.getMessageType() != FlowMessageType.COPY_TYPE) { + errorMessage = "数据验证失败,当前消息不是抄送类型消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowMessage.getOnlineFormData() == null || !flowMessage.getOnlineFormData()) { + errorMessage = "数据验证失败,当前消息为静态路由表单数据,不能通过该接口获取!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowMessageService.isCandidateIdentityOnMessage(messageId)) { + errorMessage = "数据验证失败,当前用户没有权限访问该消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + HistoricProcessInstance instance = + flowApiService.getHistoricProcessInstance(flowMessage.getProcessInstanceId()); + // 如果业务主数据为空,则直接返回。 + if (StrUtil.isBlank(instance.getBusinessKey())) { + errorMessage = "数据验证失败,当前消息为所属流程实例没有包含业务主键Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Long formId = Long.valueOf(flowMessage.getBusinessDataShot()); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(formId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + JSONObject jsonData = this.buildUserTaskData( + instance.getBusinessKey(), datasource, relationListResult.getData()); + // 将当前消息更新为已读 + flowMessageService.readCopyTask(messageId); + return ResponseResult.success(jsonData); + } + + /** + * 工作流工单列表。 + * + * @param processDefinitionKey 流程标识名。 + * @param flowWorkOrderDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 查询结果。 + */ + @SaTokenDenyAuth + @PostMapping("/listWorkOrder/{processDefinitionKey}") + public ResponseResult> listWorkOrder( + @PathVariable("processDefinitionKey") String processDefinitionKey, + @MyRequestBody FlowWorkOrderDto flowWorkOrderDtoFilter, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + FlowWorkOrder flowWorkOrderFilter = + flowOperationHelper.makeWorkOrderFilter(flowWorkOrderDtoFilter, processDefinitionKey); + MyOrderParam orderParam = new MyOrderParam(); + orderParam.add(new MyOrderParam.OrderInfo("workOrderId", false, null)); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowWorkOrder.class); + List flowWorkOrderList = + flowWorkOrderService.getFlowWorkOrderList(flowWorkOrderFilter, orderBy); + MyPageData resultData = + MyPageUtil.makeResponseData(flowWorkOrderList, FlowWorkOrderVo.class); + flowOperationHelper.buildWorkOrderApprovalStatus(processDefinitionKey, resultData.getDataList()); + // 根据工单的提交用户名获取用户的显示名称,便于前端显示。 + // 同时这也是一个如何通过插件方法,将loginName映射到showName的示例, + flowWorkOrderService.fillUserShowNameByLoginName(resultData.getDataList()); + // 工单自身的查询中可以受到数据权限的过滤,但是工单集成业务数据时,则无需再对业务数据进行数据权限过滤了。 + GlobalThreadLocal.setDataFilter(false); + ResponseResult responseResult = this.makeWorkOrderTaskInfo(resultData.getDataList()); + if (!responseResult.isSuccess()) { + return ResponseResult.errorFrom(responseResult); + } + return ResponseResult.success(resultData); + } + + /** + * 为数据源主表字段上传文件。 + * + * @param processDefinitionKey 流程引擎流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param uploadFile 上传文件对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/upload") + public void upload( + @RequestParam String processDefinitionKey, + @RequestParam(required = false) String processInstanceId, + @RequestParam(required = false) String taskId, + @RequestParam Long datasourceId, + @RequestParam(required = false) Long relationId, + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + ResponseResult verifyResult = + this.verifyUploadOrDownload(processDefinitionKey, processInstanceId, taskId, datasourceId); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyResult)); + return; + } + ResponseResult verifyTableResult = + this.verifyAndGetOnlineTable(datasourceId, relationId, null, null); + if (!verifyTableResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyTableResult)); + return; + } + onlineOperationHelper.doUpload(verifyTableResult.getData(), fieldName, asImage, uploadFile); + } + + /** + * 下载文件接口。 + * 越权访问限制说明: + * taskId为空,当前用户必须为当前流程的发起人,否则必须为当前任务的指派人或候选人。 + * relationId为空,下载数据为主表字段,否则为关联的从表字段。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param processDefinitionKey 流程引擎流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @param dataId 附件所在记录的主键Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param response Http 应答对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/download") + public void download( + @RequestParam String processDefinitionKey, + @RequestParam(required = false) String processInstanceId, + @RequestParam(required = false) String taskId, + @RequestParam Long datasourceId, + @RequestParam(required = false) Long relationId, + @RequestParam(required = false) String dataId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) throws IOException { + ResponseResult verifyResult = + this.verifyUploadOrDownload(processDefinitionKey, processInstanceId, taskId, datasourceId); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyResult)); + return; + } + ResponseResult verifyTableResult = + this.verifyAndGetOnlineTable(datasourceId, relationId, verifyResult.getData(), dataId); + if (!verifyTableResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(verifyTableResult)); + return; + } + onlineOperationHelper.doDownload(verifyTableResult.getData(), dataId, fieldName, filename, asImage, response); + } + + /** + * 获取所有流程对象,同时获取关联的在线表单对象列表。 + * + * @return 查询结果。 + */ + @GetMapping("/listFlowEntryForm") + public ResponseResult> listFlowEntryForm() { + List flowEntryList = flowEntryService.getFlowEntryList(null, null); + List flowEntryVoList = MyModelUtil.copyCollectionTo(flowEntryList, FlowEntryVo.class); + if (CollUtil.isNotEmpty(flowEntryVoList)) { + Set pageIdSet = flowEntryVoList.stream().map(FlowEntryVo::getPageId).collect(Collectors.toSet()); + List formList = onlineFormService.getOnlineFormListByPageIds(pageIdSet); + formList.forEach(f -> f.setWidgetJson(null)); + Map> formMap = + formList.stream().collect(Collectors.groupingBy(OnlineForm::getPageId)); + for (FlowEntryVo flowEntryVo : flowEntryVoList) { + List flowEntryFormList = formMap.get(flowEntryVo.getPageId()); + flowEntryVo.setFormList(MyModelUtil.beanToMapList(flowEntryFormList)); + } + } + return ResponseResult.success(flowEntryVoList); + } + + /** + * 获取在线表单工作流Id所关联的权限数据,包括权限字列表和权限资源列表。 + * 注:该接口仅用于微服务间调用使用,无需对前端开放。 + * + * @param onlineFlowEntryIds 在线表单工作流Id集合。 + * @return 参数中在线表单工作流Id集合所关联的权限数据。 + */ + @GetMapping("/calculatePermData") + public ResponseResult>> calculatePermData(@RequestParam Set onlineFlowEntryIds) { + return ResponseResult.success(flowOnlineOperationService.calculatePermData(onlineFlowEntryIds)); + } + + private ResponseResult startAndTake( + String processDefinitionKey, + FlowTaskCommentDto flowTaskCommentDto, + JSONObject taskVariableData, + JSONObject masterData, + JSONObject slaveData, + JSONObject copyData) { + ResponseResult> verifyResult = + this.verifyAndGetFlowEntryPublishAndDatasource(processDefinitionKey, true); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntryPublish flowEntryPublish = verifyResult.getData().getFirst(); + OnlineDatasource datasource = verifyResult.getData().getSecond(); + OnlineTable masterTable = datasource.getMasterTable(); + // 这里把传阅数据放到任务变量中,是为了避免给流程数据操作方法增加额外的方法调用参数。 + if (MapUtil.isNotEmpty(copyData)) { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.COPY_DATA_KEY, copyData); + } + FlowTaskComment flowTaskComment = BeanUtil.copyProperties(flowTaskCommentDto, FlowTaskComment.class); + // 保存在线表单提交的数据,同时启动流程和自动完成第一个用户任务。 + if (slaveData == null) { + flowOnlineOperationService.saveNewAndStartProcess( + flowEntryPublish.getProcessDefinitionId(), + flowTaskComment, + taskVariableData, + masterTable, + masterData); + } else { + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + flowOnlineOperationService.saveNewAndStartProcess( + flowEntryPublish.getProcessDefinitionId(), + flowTaskComment, + taskVariableData, + masterTable, + masterData, + slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + private ResponseResult verifyAndGetOnlineDatasource(Long formId) { + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + if (CollUtil.isEmpty(formDatasourceList)) { + String errorMessage = "数据验证失败,流程任务绑定的在线表单Id [" + formId + "] 不存在,请修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return onlineOperationHelper.verifyAndGetDatasource(formDatasourceList.get(0).getDatasourceId()); + } + + private ResponseResult> verifyAndGetFlowEntryPublishAndDatasource( + String processDefinitionKey, boolean checkStarter) { + String errorMessage; + // 1. 验证流程数据的合法性。 + ResponseResult flowEntryResult = flowOperationHelper.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + // 2. 验证流程一个用户任务的合法性。 + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + if (BooleanUtil.isFalse(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布对象已被挂起,不能启动新流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult taskInfoResult = + flowOperationHelper.verifyAndGetInitialTaskInfo(flowEntryPublish, checkStarter); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + // 3. 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + return ResponseResult.success(new Tuple2<>(flowEntryPublish, datasourceResult.getData())); + } + + private ResponseResult verifyAndGetOnlineTable( + Long datasourceId, Long relationId, String businessKey, String dataId) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineTable masterTable = datasourceResult.getData().getMasterTable(); + OnlineTable table = masterTable; + ResponseResult relationResult = null; + if (relationId != null) { + relationResult = onlineOperationHelper.verifyAndGetRelation(datasourceId, relationId); + if (!relationResult.isSuccess()) { + return ResponseResult.errorFrom(relationResult); + } + table = relationResult.getData().getSlaveTable(); + } + if (StrUtil.hasBlank(businessKey, dataId)) { + return ResponseResult.success(table); + } + String errorMessage; + // 如果relationId为null,这里就是主表数据。 + if (relationId == null) { + if (!StrUtil.equals(businessKey, dataId)) { + errorMessage = "数据验证失败,参数主键Id与流程主表主键Id不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(table); + } + OnlineDatasourceRelation relation = relationResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + Map dataMap = + onlineOperationService.getMasterData(slaveTable, null, null, dataId); + if (dataMap == null) { + errorMessage = "数据验证失败,从表主键Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineColumn slaveColumn = relation.getSlaveColumn(); + Object relationSlaveDataId = dataMap.get(slaveColumn.getColumnName()); + if (relationSlaveDataId == null) { + errorMessage = "数据验证失败,当前关联的从表字段值为NULL!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + if (BooleanUtil.isTrue(masterColumn.getPrimaryKey()) + && !StrUtil.equals(relationSlaveDataId.toString(), businessKey)) { + errorMessage = "数据验证失败,当前从表主键Id关联的主表Id当前流程的BusinessKey不一致!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Map masterDataMap = + onlineOperationService.getMasterData(masterTable, null, null, businessKey); + if (masterDataMap == null) { + errorMessage = "数据验证失败,主表主键Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Object relationMasterDataId = masterDataMap.get(masterColumn.getColumnName()); + if (relationMasterDataId == null) { + errorMessage = "数据验证失败,当前关联的主表字段值为NULL!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(relationMasterDataId.toString(), relationSlaveDataId.toString())) { + errorMessage = "数据验证失败,当前关联的主表字段值和从表字段值不一致!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(table); + } + + private ResponseResult verifyUploadOrDownload( + String processDefinitionKey, String processInstanceId, String taskId, Long datasourceId) { + if (!StrUtil.isAllBlank(processInstanceId, taskId)) { + ResponseResult verifyResult = + flowOperationHelper.verifyUploadOrDownloadPermission(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(ResponseResult.errorFrom(verifyResult)); + } + } + String errorMessage; + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (flowEntry == null) { + errorMessage = "数据验证失败,指定流程Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String businessKey = null; + if (processInstanceId != null) { + HistoricProcessInstance instance = flowApiService.getHistoricProcessInstance(processInstanceId); + if (!StrUtil.equals(flowEntry.getProcessDefinitionKey(), instance.getProcessDefinitionKey())) { + errorMessage = "数据验证失败,指定流程实例并不属于当前流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + businessKey = instance.getBusinessKey(); + } + List datasourceList = + onlinePageService.getOnlinePageDatasourceListByPageId(flowEntry.getPageId()); + Optional r = datasourceList.stream() + .map(OnlinePageDatasource::getDatasourceId).filter(c -> c.equals(datasourceId)).findFirst(); + if (r.isEmpty()) { + errorMessage = "数据验证失败,当前数据源Id并不属于当前流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(businessKey); + } + + private ResponseResult submitNewTask( + String instanceId, + String taskId, + FlowTaskComment comment, + JSONObject variableData, + OnlineDatasource datasource, + JSONObject masterData, + JSONObject slaveData) { + OnlineTable masterTable = datasource.getMasterTable(); + // 保存在线表单提交的数据,同时启动流程和自动完成第一个用户任务。 + if (slaveData == null) { + flowOnlineOperationService.saveNewAndTakeTask( + instanceId, taskId, comment, variableData, masterTable, masterData); + } else { + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + flowOnlineOperationService.saveNewAndTakeTask( + instanceId, taskId, comment, variableData, masterTable, masterData, slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + private JSONObject buildUserTaskData( + String businessKey, OnlineDatasource datasource, List relationList) { + OnlineTable masterTable = datasource.getMasterTable(); + JSONObject jsonData = new JSONObject(); + List oneToOneRelationList = relationList.stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + Map result = + onlineOperationService.getMasterData(masterTable, oneToOneRelationList, relationList, businessKey); + if (MapUtil.isEmpty(result)) { + return jsonData; + } + jsonData.put(datasource.getVariableName(), result); + List oneToManyRelationList = relationList.stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_MANY)).collect(Collectors.toList()); + if (CollUtil.isEmpty(oneToManyRelationList)) { + return jsonData; + } + for (OnlineDatasourceRelation relation : oneToManyRelationList) { + OnlineFilterDto filterDto = new OnlineFilterDto(); + filterDto.setTableName(relation.getSlaveTable().getTableName()); + OnlineColumn slaveColumn = relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + filterDto.setColumnName(slaveColumn.getColumnName()); + filterDto.setFilterType(FieldFilterType.EQUAL_FILTER); + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + Object columnValue = result.get(masterColumn.getColumnName()); + filterDto.setColumnValue(columnValue); + MyPageData> pageData = onlineOperationService.getSlaveDataList( + relation, CollUtil.newLinkedList(filterDto), null, null); + if (CollUtil.isNotEmpty(pageData.getDataList())) { + result.put(relation.getVariableName() + ONE_TO_MANY_VAR_SUFFIX, pageData.getDataList()); + } + } + return jsonData; + } + + private JSONObject buildDraftData( + OnlineDatasource datasource, + JSONObject masterData, + List relationList, + JSONObject slaveData) { + OnlineTable masterTable = datasource.getMasterTable(); + JSONObject jsonData = new JSONObject(); + JSONObject normalizedMasterData = new JSONObject(); + Map columnNameAndColumnMap = masterTable.getColumnMap() + .values().stream().collect(Collectors.toMap(OnlineColumn::getColumnName, c -> c)); + if (masterData != null) { + for (Map.Entry entry : masterData.entrySet()) { + OnlineColumn column = columnNameAndColumnMap.get(entry.getKey()); + Object v = onlineOperationHelper.convertToTypeValue(column, entry.getValue().toString()); + normalizedMasterData.put(entry.getKey(), v); + } + } + if (slaveData != null && relationList != null) { + Map relationMap = + relationList.stream().collect(Collectors.toMap(OnlineDatasourceRelation::getRelationId, c -> c)); + for (Map.Entry entry : slaveData.entrySet()) { + OnlineDatasourceRelation relation = relationMap.get(Long.valueOf(entry.getKey())); + if (relation != null) { + this.buildRelationDraftData(relation, entry.getValue(), normalizedMasterData); + } + } + } + jsonData.put(datasource.getVariableName(), normalizedMasterData); + return jsonData; + } + + private void buildRelationDraftData(OnlineDatasourceRelation relation, Object value, JSONObject masterData) { + if (relation.getRelationType().equals(RelationType.ONE_TO_ONE)) { + Map slaveColumnNameAndColumnMap = + relation.getSlaveTable().getColumnMap().values() + .stream().collect(Collectors.toMap(OnlineColumn::getColumnName, c -> c)); + JSONObject slaveObject = (JSONObject) value; + JSONObject normalizedSlaveObject = new JSONObject(); + for (Map.Entry entry2 : slaveObject.entrySet()) { + OnlineColumn column = slaveColumnNameAndColumnMap.get(entry2.getKey()); + Object v = onlineOperationHelper.convertToTypeValue(column, entry2.getValue().toString()); + normalizedSlaveObject.put(entry2.getKey(), v); + } + masterData.put(relation.getVariableName(), normalizedSlaveObject); + } else if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + JSONArray slaveArray = (JSONArray) value; + JSONArray normalizedSlaveArray = new JSONArray(); + for (int i = 0; i <= slaveArray.size() - 1; i++) { + JSONObject slaveObject = slaveArray.getJSONObject(i); + JSONObject normalizedSlaveObject = new JSONObject(); + normalizedSlaveObject.putAll(slaveObject); + normalizedSlaveArray.add(normalizedSlaveObject); + } + masterData.put(relation.getVariableName(), normalizedSlaveArray); + } + } + + private ResponseResult makeWorkOrderTaskInfo(List flowWorkOrderVoList) { + if (CollUtil.isEmpty(flowWorkOrderVoList)) { + return ResponseResult.success(); + } + Set definitionIdSet = + flowWorkOrderVoList.stream().map(FlowWorkOrderVo::getProcessDefinitionId).collect(Collectors.toSet()); + List flowEntryPublishList = flowEntryService.getFlowEntryPublishList(definitionIdSet); + Map flowEntryPublishMap = + flowEntryPublishList.stream().collect(Collectors.toMap(FlowEntryPublish::getProcessDefinitionId, c -> c)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + FlowEntryPublish flowEntryPublish = flowEntryPublishMap.get(flowWorkOrderVo.getProcessDefinitionId()); + flowWorkOrderVo.setInitTaskInfo(flowEntryPublish.getInitTaskInfo()); + } + Long tableId = flowWorkOrderVoList.get(0).getOnlineTableId(); + OnlineTable masterTable = onlineTableService.getOnlineTableFromCache(tableId); + ResponseResult responseResult = + this.buildWorkOrderMasterData(flowWorkOrderVoList, masterTable); + if (!responseResult.isSuccess()) { + return ResponseResult.errorFrom(responseResult); + } + responseResult = this.buildWorkOrderDraftData(flowWorkOrderVoList, masterTable); + if (!responseResult.isSuccess()) { + return ResponseResult.errorFrom(responseResult); + } + List unfinishedProcessInstanceIds = flowWorkOrderVoList.stream() + .filter(c -> !c.getFlowStatus().equals(FlowTaskStatus.FINISHED)) + .map(FlowWorkOrderVo::getProcessInstanceId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(unfinishedProcessInstanceIds)) { + return ResponseResult.success(); + } + Map> taskMap = + flowApiService.getTaskListByProcessInstanceIds(unfinishedProcessInstanceIds) + .stream().collect(Collectors.groupingBy(Task::getProcessInstanceId)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + List instanceTaskList = taskMap.get(flowWorkOrderVo.getProcessInstanceId()); + if (instanceTaskList != null) { + JSONArray taskArray = new JSONArray(); + for (Task task : instanceTaskList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("taskId", task.getId()); + jsonObject.put("taskName", task.getName()); + jsonObject.put("taskKey", task.getTaskDefinitionKey()); + jsonObject.put("assignee", task.getAssignee()); + taskArray.add(jsonObject); + } + flowWorkOrderVo.setRuntimeTaskInfoList(taskArray); + } + } + return ResponseResult.success(); + } + + private ResponseResult buildWorkOrderDraftData( + List flowWorkOrderVoList, OnlineTable masterTable) { + List draftWorkOrderList = flowWorkOrderVoList.stream() + .filter(c -> c.getFlowStatus().equals(FlowTaskStatus.DRAFT)).collect(Collectors.toList()); + if (CollUtil.isEmpty(draftWorkOrderList)) { + return ResponseResult.success(); + } + Set workOrderIdSet = draftWorkOrderList.stream() + .map(FlowWorkOrderVo::getWorkOrderId).collect(Collectors.toSet()); + List workOrderExtList = + flowWorkOrderService.getFlowWorkOrderExtByWorkOrderIds(workOrderIdSet); + Map workOrderExtMap = workOrderExtList.stream() + .collect(Collectors.toMap(FlowWorkOrderExt::getWorkOrderId, c -> c)); + for (FlowWorkOrderVo workOrder : draftWorkOrderList) { + FlowWorkOrderExt workOrderExt = workOrderExtMap.get(workOrder.getWorkOrderId()); + if (workOrderExt == null) { + continue; + } + JSONObject draftData = JSON.parseObject(workOrderExt.getDraftData()); + JSONObject masterData = draftData.getJSONObject(FlowConstant.MASTER_DATA_KEY); + JSONObject slaveData = draftData.getJSONObject(FlowConstant.SLAVE_DATA_KEY); + OnlineDatasource datasource = + onlineDatasourceService.getOnlineDatasourceByMasterTableId(masterTable.getTableId()); + List slaveRelationList = null; + if (slaveData != null) { + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), RelationType.ONE_TO_ONE); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + slaveRelationList = relationListResult.getData(); + } + datasource.setMasterTable(masterTable); + JSONObject jsonData = this.buildDraftData(datasource, masterData, slaveRelationList, slaveData); + JSONObject masterAndOneToOneData = jsonData.getJSONObject(datasource.getVariableName()); + if (MapUtil.isNotEmpty(masterAndOneToOneData)) { + List> dataList = new LinkedList<>(); + dataList.add(masterAndOneToOneData); + onlineOperationService.buildDataListWithDict(masterTable, slaveRelationList, dataList); + } + workOrder.setMasterData(masterAndOneToOneData); + } + return ResponseResult.success(); + } + + private ResponseResult buildWorkOrderMasterData( + List flowWorkOrderVoList, OnlineTable masterTable) { + Set businessKeySet = flowWorkOrderVoList.stream() + .map(FlowWorkOrderVo::getBusinessKey) + .filter(Objects::nonNull).collect(Collectors.toSet()); + if (CollUtil.isEmpty(businessKeySet)) { + return ResponseResult.success(); + } + Set convertedBusinessKeySet = + onlineOperationHelper.convertToTypeValue(masterTable.getPrimaryKeyColumn(), businessKeySet); + List filterList = new LinkedList<>(); + OnlineFilterDto filterDto = new OnlineFilterDto(); + filterDto.setTableName(masterTable.getTableName()); + filterDto.setColumnName(masterTable.getPrimaryKeyColumn().getColumnName()); + filterDto.setFilterType(FieldFilterType.IN_LIST_FILTER); + filterDto.setColumnValueList(new HashSet<>(convertedBusinessKeySet)); + filterList.add(filterDto); + TaskInfoVo taskInfoVo = JSON.parseObject(flowWorkOrderVoList.get(0).getInitTaskInfo(), TaskInfoVo.class); + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfoVo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), RelationType.ONE_TO_ONE); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + MyPageData> pageData = onlineOperationService.getMasterDataList( + masterTable, relationListResult.getData(), null, filterList, null, null); + List> dataList = pageData.getDataList(); + Map> dataMap = dataList.stream() + .collect(Collectors.toMap(c -> c.get(masterTable.getPrimaryKeyColumn().getColumnName()).toString(), c -> c)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + if (StrUtil.isNotBlank(flowWorkOrderVo.getBusinessKey())) { + Object dataId = onlineOperationHelper.convertToTypeValue( + masterTable.getPrimaryKeyColumn(), flowWorkOrderVo.getBusinessKey()); + Map data = dataMap.get(dataId.toString()); + if (data != null) { + flowWorkOrderVo.setMasterData(data); + } + } + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java new file mode 100644 index 00000000..14aee423 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/FlowOnlineOperationService.java @@ -0,0 +1,136 @@ +package com.orangeforms.common.flow.online.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import org.flowable.task.api.Task; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 流程操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowOnlineOperationService { + + /** + * 保存在线表单的数据,同时启动流程。如果当前用户是第一个用户任务的Assignee, + * 或者第一个用户任务的Assignee是流程发起人变量,该方法还会自动Take第一个任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param table 表对象。 + * @param data 表数据。 + */ + void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data); + + /** + * 保存在线表单的数据,同时启动流程。如果当前用户是第一个用户任务的Assignee, + * 或者第一个用户任务的Assignee是流程发起人变量,该方法还会自动Take第一个任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param slaveDataListMap 关联从表数据Map。 + */ + void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap); + + /** + * 保存在线表单的草稿数据,同时启动一个流程实例。 + * + * @param processDefinitionId 流程定义Id。 + * @param tableId 在线表单主表Id。 + * @param masterData 主表数据。 + * @param slaveData 所有关联从表数据。 + * @return 流程工单对象。 + */ + FlowWorkOrder saveNewDraftAndStartProcess( + String processDefinitionId, Long tableId, JSONObject masterData, JSONObject slaveData); + + /** + * 保存在线表单的数据,同时Take用户任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param table 表对象。 + * @param data 表数据。 + */ + void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data); + + /** + * 保存在线表单的数据,同时Take用户任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param slaveDataListMap 关联从表数据Map。 + */ + void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap); + + /** + * 保存业务表数据,同时接收流程任务。 + * + * @param task 流程任务。 + * @param flowTaskComment 流程审批批注对象。 + * @param taskVariableData 流程任务的变量数据。 + * @param datasource 主表所在数据源。 + * @param masterData 主表数据。 + * @param masterDataId 主表数据主键。 + * @param slaveDataListMap 从表数据。 + */ + void updateAndTakeTask( + Task task, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineDatasource datasource, + JSONObject masterData, + String masterDataId, + Map> slaveDataListMap); + + /** + * 获取在线表单工作流Id所关联的权限数据,包括权限字列表和权限资源列表。 + * + * @param onlineFormEntryIds 在线表单工作流Id集合。 + * @return 参数中在线表单工作流Id集合所关联的权限数据。 + */ + List> calculatePermData(Set onlineFormEntryIds); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java new file mode 100644 index 00000000..4dba8ac7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineBusinessServiceImpl.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.flow.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.flow.base.service.BaseFlowOnlineService; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.online.exception.OnlineRuntimeException; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.online.service.OnlineTableService; +import jakarta.annotation.PostConstruct; +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.List; + +/** + * 在线表单和流程监听器进行数据对接时的服务实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSource(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowOnlineBusinessService") +public class FlowOnlineBusinessServiceImpl implements BaseFlowOnlineService { + + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineOperationService onlineOperationService; + + @PostConstruct + public void doRegister() { + flowCustomExtFactory.getOnlineBusinessDataExtHelper().setOnlineBusinessService(this); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFlowStatus(FlowWorkOrder workOrder) { + OnlineTable onlineTable = onlineTableService.getOnlineTableFromCache(workOrder.getOnlineTableId()); + if (onlineTable == null) { + log.error("OnlineTableId [{}] doesn't exist while calling FlowOnlineBusinessServiceImpl.updateFlowStatus", + workOrder.getOnlineTableId()); + return; + } + String dataId = workOrder.getBusinessKey(); + for (OnlineColumn column : onlineTable.getColumnMap().values()) { + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.FLOW_FINISHED_STATUS)) { + onlineOperationService.updateColumn(onlineTable, dataId, column, workOrder.getFlowStatus()); + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.FLOW_APPROVAL_STATUS)) { + onlineOperationService.updateColumn(onlineTable, dataId, column, workOrder.getLatestApprovalStatus()); + } + } + } + + @Override + public void deleteBusinessData(FlowWorkOrder workOrder) { + OnlineTable onlineTable = onlineTableService.getOnlineTableFromCache(workOrder.getOnlineTableId()); + if (onlineTable == null) { + log.error("OnlineTableId [{}] doesn't exist while calling FlowOnlineBusinessServiceImpl.deleteBusinessData", + workOrder.getOnlineTableId()); + return; + } + OnlineDatasource datasource = + onlineDatasourceService.getOnlineDatasourceByMasterTableId(onlineTable.getTableId()); + List relationList = + onlineDatasourceRelationService.getOnlineDatasourceRelationListFromCache(CollUtil.newHashSet(datasource.getDatasourceId())); + String dataId = workOrder.getBusinessKey(); + for (OnlineDatasourceRelation relation : relationList) { + OnlineTable slaveTable = onlineTableService.getOnlineTableFromCache(relation.getSlaveTableId()); + if (slaveTable == null) { + throw new OnlineRuntimeException("数据验证失败,数据源关联 [" + relation.getRelationName() + "] 的从表Id不存在!"); + } + relation.setSlaveTable(slaveTable); + } + onlineOperationService.delete(onlineTable, relationList, dataId); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java new file mode 100644 index 00000000..514343ca --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/java/com/orangeforms/common/flow/online/service/impl/FlowOnlineOperationServiceImpl.java @@ -0,0 +1,287 @@ +package com.orangeforms.common.flow.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MultiDatabaseWriteMethod; +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.flow.config.FlowProperties; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.model.FlowEntry; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.online.service.FlowOnlineOperationService; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineOperationService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSource(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowOnlineOperationService") +public class FlowOnlineOperationServiceImpl implements FlowOnlineOperationService { + + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private FlowProperties flowProperties; + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data) { + this.saveNewAndStartProcess(processDefinitionId, flowTaskComment, taskVariableData, table, data, null); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndStartProcess( + String processDefinitionId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap) { + Object dataId = onlineOperationService.saveNewWithRelation(masterTable, masterData, slaveDataListMap); + Assert.notNull(dataId); + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.MASTER_DATA_KEY, masterData); + taskVariableData.put(FlowConstant.SLAVE_DATA_KEY, this.normailizeSlaveDataListMap(slaveDataListMap)); + taskVariableData.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + ProcessInstance instance = flowApiService.start(processDefinitionId, dataId); + flowWorkOrderService.saveNew(instance, dataId, masterTable.getTableId(), null); + flowApiService.takeFirstTask(instance.getProcessInstanceId(), flowTaskComment, taskVariableData); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowWorkOrder saveNewDraftAndStartProcess( + String processDefinitionId, Long tableId, JSONObject masterData, JSONObject slaveData) { + ProcessInstance instance = flowApiService.start(processDefinitionId, null); + return flowWorkOrderService.saveNewWithDraft( + instance, tableId, null, JSON.toJSONString(masterData), JSON.toJSONString(slaveData)); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable table, + JSONObject data) { + this.saveNewAndTakeTask( + processInstanceId, taskId, flowTaskComment, taskVariableData, table, data, null); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAndTakeTask( + String processInstanceId, + String taskId, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap) { + Object dataId = onlineOperationService.saveNewWithRelation(masterTable, masterData, slaveDataListMap); + Assert.notNull(dataId); + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + flowApiService.setBusinessKeyForProcessInstance(processInstanceId, dataId); + Map variables = + flowApiService.initAndGetProcessInstanceVariables(task.getProcessDefinitionId()); + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.putAll(variables); + taskVariableData.put(FlowConstant.MASTER_DATA_KEY, masterData); + taskVariableData.put(FlowConstant.SLAVE_DATA_KEY, this.normailizeSlaveDataListMap(slaveDataListMap)); + taskVariableData.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + flowApiService.completeTask(task, flowTaskComment, taskVariableData); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + FlowWorkOrder flowWorkOrder = + flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(instance.getProcessInstanceId()); + if (flowWorkOrder == null) { + flowWorkOrderService.saveNew(instance, dataId, masterTable.getTableId(), null); + } else { + flowWorkOrder.setBusinessKey(dataId.toString()); + flowWorkOrder.setUpdateTime(new Date()); + flowWorkOrder.setFlowStatus(FlowTaskStatus.SUBMITTED); + flowWorkOrderService.updateById(flowWorkOrder); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void updateAndTakeTask( + Task task, + FlowTaskComment flowTaskComment, + JSONObject taskVariableData, + OnlineDatasource datasource, + JSONObject masterData, + String masterDataId, + Map> slaveDataListMap) { + int flowStatus = FlowTaskStatus.APPROVING; + if (flowTaskComment.getApprovalType().equals(FlowApprovalType.REFUSE)) { + flowStatus = FlowTaskStatus.REFUSED; + } else if (flowTaskComment.getApprovalType().equals(FlowApprovalType.STOP)) { + flowStatus = FlowTaskStatus.FINISHED; + } + OnlineTable masterTable = datasource.getMasterTable(); + Long datasourceId = datasource.getDatasourceId(); + flowWorkOrderService.updateFlowStatusByProcessInstanceId(task.getProcessInstanceId(), flowStatus); + this.updateMasterData(masterTable, masterData, masterDataId); + if (slaveDataListMap != null) { + for (Map.Entry> relationEntry : slaveDataListMap.entrySet()) { + Long relationId = relationEntry.getKey().getRelationId(); + onlineOperationService.updateRelationData( + masterTable, masterData, masterDataId, datasourceId, relationId, relationEntry.getValue()); + } + } + if (flowTaskComment.getApprovalType().equals(FlowApprovalType.STOP)) { + Integer s = MapUtil.getInt(taskVariableData, FlowConstant.LATEST_APPROVAL_STATUS_KEY); + flowWorkOrderService.updateLatestApprovalStatusByProcessInstanceId(task.getProcessInstanceId(), s); + CallResult stopResult = flowApiService.stopProcessInstance( + task.getProcessInstanceId(), flowTaskComment.getTaskComment(), flowStatus); + if (!stopResult.isSuccess()) { + throw new FlowOperationException(stopResult.getErrorMessage()); + } + } else { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + taskVariableData.put(FlowConstant.MASTER_DATA_KEY, masterData); + taskVariableData.put(FlowConstant.SLAVE_DATA_KEY, this.normailizeSlaveDataListMap(slaveDataListMap)); + taskVariableData.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + flowApiService.completeTask(task, flowTaskComment, taskVariableData); + } + } + + @Override + public List> calculatePermData(Set onlineFormEntryIds) { + if (CollUtil.isEmpty(onlineFormEntryIds)) { + return new LinkedList<>(); + } + List> permDataList = new LinkedList<>(); + List flowEntries = flowEntryService.getInList(onlineFormEntryIds); + Set pageIds = flowEntries.stream().map(FlowEntry::getPageId).collect(Collectors.toSet()); + Map pageAndVariableNameMap = + onlineDatasourceService.getPageIdAndVariableNameMapByPageIds(pageIds); + for (FlowEntry flowEntry : flowEntries) { + JSONObject permData = new JSONObject(); + permData.put("entryId", flowEntry.getEntryId()); + String key = StrUtil.upperFirst(flowEntry.getProcessDefinitionKey()); + List permCodeList = new LinkedList<>(); + String formPermCode = "form" + key; + permCodeList.add(formPermCode); + permCodeList.add(formPermCode + ":fragment" + key); + permData.put("permCodeList", permCodeList); + String flowUrlPrefix = flowProperties.getUrlPrefix(); + String onlineUrlPrefix = onlineProperties.getUrlPrefix(); + List permList = CollUtil.newLinkedList( + onlineUrlPrefix + "/onlineForm/view", + onlineUrlPrefix + "/onlineForm/render", + onlineUrlPrefix + "/onlineOperation/listByOneToManyRelationId/" + pageAndVariableNameMap.get(flowEntry.getPageId()), + onlineUrlPrefix + "/onlineOperation/uploadByOneToManyRelationId/" + pageAndVariableNameMap.get(flowEntry.getPageId()), + onlineUrlPrefix + "/onlineOperation/dowloadByOneToManyRelationId/" + pageAndVariableNameMap.get(flowEntry.getPageId()), + flowUrlPrefix + "/flowOperation/viewInitialHistoricTaskInfo", + flowUrlPrefix + "/flowOperation/startOnly", + flowUrlPrefix + "/flowOperation/viewInitialTaskInfo", + flowUrlPrefix + "/flowOperation/viewRuntimeTaskInfo", + flowUrlPrefix + "/flowOperation/viewProcessBpmn", + flowUrlPrefix + "/flowOperation/viewHighlightFlowData", + flowUrlPrefix + "/flowOperation/listFlowTaskComment", + flowUrlPrefix + "/flowOperation/cancelWorkOrder", + flowUrlPrefix + "/flowOperation/listRuntimeTask", + flowUrlPrefix + "/flowOperation/listHistoricProcessInstance", + flowUrlPrefix + "/flowOperation/listHistoricTask", + flowUrlPrefix + "/flowOperation/freeJumpTo", + flowUrlPrefix + "/flowOnlineOperation/startPreview", + flowUrlPrefix + "/flowOnlineOperation/viewUserTask", + flowUrlPrefix + "/flowOnlineOperation/viewHistoricProcessInstance", + flowUrlPrefix + "/flowOnlineOperation/submitUserTask", + flowUrlPrefix + "/flowOnlineOperation/upload", + flowUrlPrefix + "/flowOnlineOperation/download", + flowUrlPrefix + "/flowOperation/submitConsign", + flowUrlPrefix + "/flowOnlineOperation/startAndTakeUserTask/" + flowEntry.getProcessDefinitionKey(), + flowUrlPrefix + "/flowOnlineOperation/startAndSaveDraft/" + flowEntry.getProcessDefinitionKey(), + flowUrlPrefix + "/flowOnlineOperation/listWorkOrder/" + flowEntry.getProcessDefinitionKey(), + flowUrlPrefix + "/flowOnlineOperation/printWorkOrder/" + flowEntry.getProcessDefinitionKey() + ); + permData.put("permList", permList); + permDataList.add(permData); + } + return permDataList; + } + + private void updateMasterData(OnlineTable masterTable, JSONObject masterData, String dataId) { + if (masterData == null) { + return; + } + // 如果存在主表数据,就执行主表数据的更新。 + Map originalMasterData = + onlineOperationService.getMasterData(masterTable, null, null, dataId); + for (Map.Entry entry : originalMasterData.entrySet()) { + masterData.putIfAbsent(entry.getKey(), entry.getValue()); + } + if (!onlineOperationService.update(masterTable, masterData)) { + throw new FlowOperationException("主表数据不存在!"); + } + } + + private Map> normailizeSlaveDataListMap( + Map> slaveDataListMap) { + if (slaveDataListMap == null || slaveDataListMap.isEmpty()) { + return null; + } + Map> resultMap = new HashMap<>(slaveDataListMap.size()); + for (Map.Entry> entry : slaveDataListMap.entrySet()) { + resultMap.put(entry.getKey().getSlaveTable().getTableName(), entry.getValue()); + } + return resultMap; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..8ec96e36 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.flow.online.config.FlowOnlineAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/pom.xml new file mode 100644 index 00000000..daad1d91 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/pom.xml @@ -0,0 +1,49 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-flow + 1.0.0 + common-flow + jar + + + + com.orangeforms + common-satoken + 1.0.0 + + + com.orangeforms + common-datafilter + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-log + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + org.flowable + flowable-spring-boot-starter-process + ${flowable.version} + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java new file mode 100644 index 00000000..b7ee7293 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/advice/FlowExceptionHandler.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.flow.advice; + +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.flow.exception.FlowEmptyUserException; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.service.FlowTaskCommentService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.common.engine.api.FlowableException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 流程业务层的异常处理类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Order(1) +@RestControllerAdvice("com.orangeforms") +public class FlowExceptionHandler { + + @Autowired + private FlowTaskCommentService flowTaskCommentService; + + @ExceptionHandler(value = FlowableException.class) + public ResponseResult exceptionHandle(FlowableException ex, HttpServletRequest request) { + if (ex instanceof FlowEmptyUserException) { + FlowEmptyUserException flowEmptyUserException = (FlowEmptyUserException) ex; + FlowTaskComment comment = JSON.parseObject(flowEmptyUserException.getMessage(), FlowTaskComment.class); + flowTaskCommentService.saveNew(comment); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "下一个任务节点的审批人为空,提交被自动驳回!"); + } + log.error("Unhandled FlowException from URL [" + request.getRequestURI() + "]", ex); + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION, ex.getMessage()); + } + + @SuppressWarnings("unchecked") + private T findCause(Throwable ex, Class clazz) { + if (ex.getCause() == null) { + return null; + } + if (ex.getCause().getClass().equals(clazz)) { + return (T) ex.getCause(); + } else { + return this.findCause(ex.getCause(), clazz); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java new file mode 100644 index 00000000..c22362f9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/base/service/BaseFlowOnlineService.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.base.service; + +import com.orangeforms.common.flow.model.FlowWorkOrder; + +/** + * 工作流在线表单的服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseFlowOnlineService { + + /** + * 更新在线表单主表数据的流程状态字段值。 + * + * @param workOrder 工单对象。 + */ + void updateFlowStatus(FlowWorkOrder workOrder); + + /** + * 根据工单对象级联删除业务数据。 + * + * @param workOrder 工单对象。 + */ + void deleteBusinessData(FlowWorkOrder workOrder); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java new file mode 100644 index 00000000..bf9709a6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/CustomEngineConfigurator.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.flow.config; + +import com.orangeforms.common.core.config.DynamicDataSource; +import com.orangeforms.common.core.constant.ApplicationConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; +import org.flowable.common.engine.impl.EngineConfigurator; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * 服务启动过程中动态切换flowable引擎内置表所在的数据源。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class CustomEngineConfigurator implements EngineConfigurator { + + @Override + public void beforeInit(AbstractEngineConfiguration engineConfiguration) { + DataSource dataSource = engineConfiguration.getDataSource(); + if (dataSource instanceof TransactionAwareDataSourceProxy) { + TransactionAwareDataSourceProxy proxy = (TransactionAwareDataSourceProxy) dataSource; + DataSource targetDataSource = proxy.getTargetDataSource(); + if (targetDataSource instanceof DynamicDataSource) { + DynamicDataSource dynamicDataSource = (DynamicDataSource) targetDataSource; + Map dynamicDataSourceMap = dynamicDataSource.getResolvedDataSources(); + DataSource flowDataSource = dynamicDataSourceMap.get(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE); + if (flowDataSource != null) { + engineConfiguration.setDataSource(flowDataSource); + } + } + } + } + + @Override + public void configure(AbstractEngineConfiguration engineConfiguration) { + // 默认实现。 + } + + @Override + public int getPriority() { + return 0; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java new file mode 100644 index 00000000..a6c7345a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-flow模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({FlowProperties.class}) +public class FlowAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java new file mode 100644 index 00000000..3acf5347 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/config/FlowProperties.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.flow.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 工作流的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-flow") +public class FlowProperties { + + /** + * 工作落工单操作接口的URL前缀。 + */ + private String urlPrefix; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java new file mode 100644 index 00000000..aa4de82c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowApprovalType.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流任务触发BUTTON。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowApprovalType { + + /** + * 保存。 + */ + public static final String SAVE = "save"; + /** + * 同意。 + */ + public static final String AGREE = "agree"; + /** + * 拒绝。 + */ + public static final String REFUSE = "refuse"; + /** + * 驳回。 + */ + public static final String REJECT = "reject"; + /** + * 撤销。 + */ + public static final String REVOKE = "revoke"; + /** + * 指派。 + */ + public static final String TRANSFER = "transfer"; + /** + * 多实例会签。 + */ + public static final String MULTI_SIGN = "multi_sign"; + /** + * 会签同意。 + */ + public static final String MULTI_AGREE = "multi_agree"; + /** + * 会签拒绝。 + */ + public static final String MULTI_REFUSE = "multi_refuse"; + /** + * 会签弃权。 + */ + public static final String MULTI_ABSTAIN = "multi_abstain"; + /** + * 多实例加签。 + */ + public static final String MULTI_CONSIGN = "multi_consign"; + /** + * 多实例减签。 + */ + public static final String MULTI_MINUS_SIGN = "multi_minus_sign"; + /** + * 中止。 + */ + public static final String STOP = "stop"; + /** + * 干预。 + */ + public static final String INTERVENE = "intervene"; + /** + * 自由跳转。 + */ + public static final String FREE_JUMP = "free_jump"; + /** + * 流程复活。 + */ + public static final String REUSED = "reused"; + /** + * 流程复活。 + */ + public static final String REVIVE = "revive"; + /** + * 超时自动审批。 + */ + public static final String TIMEOUT_AUTO_COMPLETE = "timeout_auto_complete"; + /** + * 空审批人自动审批。 + */ + public static final String EMPTY_USER_AUTO_COMPLETE = "empty_user_auto_complete"; + /** + * 空审批人自动退回。 + */ + public static final String EMPTY_USER_AUTO_REJECT = "empty_user_auto_reject"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowApprovalType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java new file mode 100644 index 00000000..495831b8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBackType.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.flow.constant; + +/** + * 待办任务回退类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowBackType { + + /** + * 驳回。 + */ + public static final int REJECT = 0; + /** + * 撤回。 + */ + public static final int REVOKE = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowBackType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java new file mode 100644 index 00000000..cdb89485 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowBuiltinApprovalStatus.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.flow.constant; + +/** + * 内置的流程审批状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowBuiltinApprovalStatus { + + /** + * 同意。 + */ + public static final int AGREED = 1; + /** + * 拒绝。 + */ + public static final int REFUSED = 2; + /** + * 会签同意。 + */ + public static final int MULTI_AGREED = 3; + /** + * 会签拒绝。 + */ + public static final int MULTI_REFUSED = 4; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowBuiltinApprovalStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java new file mode 100644 index 00000000..12ccf122 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowConstant.java @@ -0,0 +1,266 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流中的常量数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowConstant { + + /** + * 标识流程实例启动用户的变量名。 + */ + public static final String START_USER_NAME_VAR = "${startUserName}"; + + /** + * 流程实例发起人变量名。 + */ + public static final String PROC_INSTANCE_INITIATOR_VAR = "initiator"; + + /** + * 流程实例中发起人用户的变量名。 + */ + public static final String PROC_INSTANCE_START_USER_NAME_VAR = "startUserName"; + + /** + * 流程任务的指定人变量。 + */ + public static final String TASK_APPOINTED_ASSIGNEE_VAR = "appointedAssignee"; + + /** + * 操作类型变量。 + */ + public static final String OPERATION_TYPE_VAR = "operationType"; + + /** + * 提交用户。 + */ + public static final String SUBMIT_USER_VAR = "submitUser"; + + /** + * 多任务拒绝数量变量。 + */ + public static final String MULTI_REFUSE_COUNT_VAR = "multiRefuseCount"; + + /** + * 多任务同意数量变量。 + */ + public static final String MULTI_AGREE_COUNT_VAR = "multiAgreeCount"; + + /** + * 多任务弃权数量变量。 + */ + public static final String MULTI_ABSTAIN_COUNT_VAR = "multiAbstainCount"; + + /** + * 会签发起任务。 + */ + public static final String MULTI_SIGN_START_TASK_VAR = "multiSignStartTask"; + + /** + * 会签任务总数量。 + */ + public static final String MULTI_SIGN_NUM_OF_INSTANCES_VAR = "multiNumOfInstances"; + + /** + * 会签任务执行的批次Id。 + */ + public static final String MULTI_SIGN_TASK_EXECUTION_ID_VAR = "taskExecutionId"; + + /** + * 多实例实例数量变量。 + */ + public static final String NUMBER_OF_INSTANCES_VAR = "nrOfInstances"; + + /** + * 多实例已完成实例数量变量。 + */ + public static final String NUMBER_OF_COMPLETED_INSTANCES_VAR = "nrOfCompletedInstances"; + + /** + * 多任务指派人列表变量。 + */ + public static final String MULTI_ASSIGNEE_LIST_VAR = "assigneeList"; + + /** + * 上级部门领导审批变量。 + */ + public static final String GROUP_TYPE_UP_DEPT_POST_LEADER_VAR = "upDeptPostLeader"; + + /** + * 本部门领导审批变量。 + */ + public static final String GROUP_TYPE_DEPT_POST_LEADER_VAR = "deptPostLeader"; + + /** + * 所有部门岗位审批变量。 + */ + public static final String GROUP_TYPE_ALL_DEPT_POST_VAR = "allDeptPost"; + + /** + * 本部门岗位审批变量。 + */ + public static final String GROUP_TYPE_SELF_DEPT_POST_VAR = "selfDeptPost"; + + /** + * 同级部门岗位审批变量。 + */ + public static final String GROUP_TYPE_SIBLING_DEPT_POST_VAR = "siblingDeptPost"; + + /** + * 上级部门岗位审批变量。 + */ + public static final String GROUP_TYPE_UP_DEPT_POST_VAR = "upDeptPost"; + + /** + * 任意部门关联的岗位审批变量。 + */ + public static final String GROUP_TYPE_DEPT_POST_VAR = "deptPost"; + + /** + * 指定角色分组变量。 + */ + public static final String GROUP_TYPE_ROLE_VAR = "role"; + + /** + * 指定部门分组变量。 + */ + public static final String GROUP_TYPE_DEPT_VAR = "dept"; + + /** + * 指定用户分组变量。 + */ + public static final String GROUP_TYPE_USER_VAR = "user"; + + /** + * 指定审批人。 + */ + public static final String GROUP_TYPE_ASSIGNEE = "ASSIGNEE"; + + /** + * 岗位。 + */ + public static final String GROUP_TYPE_POST = "POST"; + + /** + * 上级部门领导审批。 + */ + public static final String GROUP_TYPE_UP_DEPT_POST_LEADER = "UP_DEPT_POST_LEADER"; + + /** + * 本部门岗位领导审批。 + */ + public static final String GROUP_TYPE_DEPT_POST_LEADER = "DEPT_POST_LEADER"; + + /** + * 本部门岗位前缀。 + */ + public static final String SELF_DEPT_POST_PREFIX = "SELF_DEPT_"; + + /** + * 上级部门岗位前缀。 + */ + public static final String UP_DEPT_POST_PREFIX = "UP_DEPT_"; + + /** + * 同级部门岗位前缀。 + */ + public static final String SIBLING_DEPT_POST_PREFIX = "SIBLING_DEPT_"; + + /** + * 当前流程实例所有任务的抄送数据前缀。 + */ + public static final String COPY_DATA_MAP_PREFIX = "copyDataMap_"; + + /** + * 作为临时变量存入任务变量JSONObject对象时的key。 + */ + public static final String COPY_DATA_KEY = "copyDataKey"; + + /** + * 流程中业务快照数据中,主表数据的Key。 + */ + public static final String MASTER_DATA_KEY = "masterData"; + + /** + * 流程中业务快照数据中,关联从表数据的Key。 + */ + public static final String SLAVE_DATA_KEY = "slaveData"; + + /** + * 流程任务的最近更新状态的Key。 + */ + public static final String LATEST_APPROVAL_STATUS_KEY = "latestApprovalStatus"; + + /** + * 流程用户任务待办之前的通知类型的Key。 + */ + public static final String USER_TASK_NOTIFY_TYPES_KEY = "flowNotifyTypeList"; + + /** + * 流程用户任务自动跳过类型的Key。 + */ + public static final String USER_TASK_AUTO_SKIP_KEY = "autoSkipType"; + + /** + * 流程用户任务驳回类型的Key。 + */ + public static final String USER_TASK_REJECT_TYPE_KEY = "rejectType"; + + /** + * 驳回时携带的变量数据。 + */ + public static final String REJECT_TO_SOURCE_DATA_VAR = "rejectData"; + + /** + * 驳回时携带的变量数据。 + */ + public static final String REJECT_BACK_TO_SOURCE_DATA_VAR = "rejectBackData"; + + /** + * 指定审批人。 + */ + public static final String DELEGATE_ASSIGNEE_VAR = "defaultAssignee"; + + /** + * 业务主表对象的键。目前仅仅用户在线表单工作流。 + */ + public static final String MASTER_TABLE_KEY = "masterTable"; + + /** + * 不删除任务超时作业。 + */ + public static final String NOT_DELETE_TIMEOUT_TASK_JOB_KEY = "notDeleteTimeoutTaskJob"; + + /** + * 用户任务超时小时数。 + */ + public static final String TASK_TIMEOUT_HOURS = "timeoutHours"; + + /** + * 用户任务超时处理方式。 + */ + public static final String TASK_TIMEOUT_HANDLE_WAY = "timeoutHandleWay"; + + /** + * 用户任务超时指定审批人。 + */ + public static final String TASK_TIMEOUT_DEFAULT_ASSIGNEE = "defaultAssignee"; + + /** + * 空处理人处理方式。 + */ + public static final String EMPTY_USER_HANDLE_WAY = "emptyUserHandleWay"; + + /** + * 空处理人时指定的审批人。 + */ + public static final String EMPTY_USER_TO_ASSIGNEE = "emptyUserToAssignee"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowConstant() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java new file mode 100644 index 00000000..d25ec6e4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskStatus.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流任务类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowTaskStatus { + + /** + * 已提交。 + */ + public static final int SUBMITTED = 0; + /** + * 审批中。 + */ + public static final int APPROVING = 1; + /** + * 被拒绝。 + */ + public static final int REFUSED = 2; + /** + * 已结束。 + */ + public static final int FINISHED = 3; + /** + * 提前停止。 + */ + public static final Integer STOPPED = 4; + /** + * 已取消。 + */ + public static final Integer CANCELLED = 5; + /** + * 保存草稿。 + */ + public static final Integer DRAFT = 6; + /** + * 流程复活。 + */ + public static final Integer REVIVE = 7; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowTaskStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java new file mode 100644 index 00000000..8d97ba9b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/constant/FlowTaskType.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.flow.constant; + +/** + * 工作流任务类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowTaskType { + + /** + * 其他类型任务。 + */ + public static final int OTHER_TYPE = 0; + /** + * 用户任务。 + */ + public static final int USER_TYPE = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowTaskType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java new file mode 100644 index 00000000..95558d08 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowCategoryController.java @@ -0,0 +1,232 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.flow.dto.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.vo.*; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * 工作流流程分类接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程分类接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowCategory") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowCategoryController { + + @Autowired + private FlowCategoryService flowCategoryService; + @Autowired + private FlowEntryService flowEntryService; + + /** + * 新增FlowCategory数据。 + * + * @param flowCategoryDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"flowCategoryDto.categoryId"}) + @SaCheckPermission("flowCategory.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody FlowCategoryDto flowCategoryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowCategoryDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowCategory flowCategory = MyModelUtil.copyTo(flowCategoryDto, FlowCategory.class); + if (flowCategoryService.existByCode(flowCategory.getCode())) { + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, "数据验证失败,当前流程分类已经存在!"); + } + flowCategory = flowCategoryService.saveNew(flowCategory); + return ResponseResult.success(flowCategory.getCategoryId()); + } + + /** + * 更新FlowCategory数据。 + * + * @param flowCategoryDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowCategory.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody FlowCategoryDto flowCategoryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowCategoryDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowCategory flowCategory = MyModelUtil.copyTo(flowCategoryDto, FlowCategory.class); + ResponseResult verifyResult = this.doVerifyAndGet(flowCategory.getCategoryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowCategory originalFlowCategory = verifyResult.getData(); + if (!StrUtil.equals(flowCategory.getCode(), originalFlowCategory.getCode())) { + FlowEntry filter = new FlowEntry(); + filter.setCategoryId(flowCategory.getCategoryId()); + filter.setStatus(FlowEntryStatus.PUBLISHED); + List flowEntryList = flowEntryService.getListByFilter(filter); + if (CollUtil.isNotEmpty(flowEntryList)) { + errorMessage = "数据验证失败,当前流程分类存在已经发布的流程数据,因此分类标识不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowCategoryService.existByCode(flowCategory.getCode())) { + errorMessage = "数据验证失败,当前流程分类已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + } + if (!flowCategoryService.update(flowCategory, originalFlowCategory)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除FlowCategory数据。 + * + * @param categoryId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowCategory.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long categoryId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(categoryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry filter = new FlowEntry(); + filter.setCategoryId(categoryId); + List flowEntryList = flowEntryService.getListByFilter(filter); + if (CollUtil.isNotEmpty(flowEntryList)) { + errorMessage = "数据验证失败,请先删除当前流程分类关联的流程数据!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowCategoryService.remove(categoryId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的FlowCategory列表。 + * + * @param flowCategoryDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("flowCategory.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody FlowCategoryDto flowCategoryDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + FlowCategory flowCategoryFilter = MyModelUtil.copyTo(flowCategoryDtoFilter, FlowCategory.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowCategory.class); + List flowCategoryList = flowCategoryService.getFlowCategoryListWithRelation(flowCategoryFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(flowCategoryList, FlowCategoryVo.class)); + } + + /** + * 查看指定FlowCategory对象详情。 + * + * @param categoryId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("flowCategory.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long categoryId) { + ResponseResult verifyResult = this.doVerifyAndGet(categoryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + return ResponseResult.success(verifyResult.getData(), FlowCategoryVo.class); + } + + /** + * 以字典形式返回全部FlowCategory数据集合。字典的键值为[categoryId, name]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject FlowCategoryDto filter) { + List resultList = + flowCategoryService.getFlowCategoryList(MyModelUtil.copyTo(filter, FlowCategory.class), null); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, FlowCategory::getCategoryId, FlowCategory::getName)); + } + + /** + * 根据字典Id集合,获取查询后的字典数据。 + * + * @param dictIds 字典Id集合。 + * @return 应答结果对象,包含字典形式的数据集合。 + */ + @GetMapping("/listDictByIds") + public ResponseResult>> listDictByIds(@RequestParam List dictIds) { + List resultList = flowCategoryService.getInList(new HashSet<>(dictIds)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, FlowCategory::getCategoryId, FlowCategory::getName)); + } + + private ResponseResult doVerifyAndGet(Long categoryId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(categoryId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + FlowCategory flowCategory = flowCategoryService.getById(categoryId); + if (flowCategory == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(flowCategory.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用并不存在该流程分类的定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(flowCategory.getTenantId(), tokenData.getTenantId())) { + errorMessage = "数据验证失败,当前租户并不存在该流程分类的定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(flowCategory); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java new file mode 100644 index 00000000..855e59de --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryController.java @@ -0,0 +1,475 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.flow.constant.FlowTaskType; +import com.orangeforms.common.flow.dto.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.vo.*; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import javax.xml.stream.XMLStreamException; +import java.util.*; + +/** + * 工作流流程定义接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程定义接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowEntry") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowEntryController { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowCategoryService flowCategoryService; + @Autowired + private FlowEntryVariableService flowEntryVariableService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowTaskExtService flowTaskExtService; + + /** + * 新增工作流对象数据。 + * + * @param flowEntryDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"flowEntryDto.entryId"}) + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody FlowEntryDto flowEntryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntry flowEntry = MyModelUtil.copyTo(flowEntryDto, FlowEntry.class); + if (flowEntryService.existByProcessDefinitionKey(flowEntry.getProcessDefinitionKey())) { + errorMessage = "数据验证失败,该流程定义标识已存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = flowEntryService.verifyRelatedData(flowEntry, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntry = flowEntryService.saveNew(flowEntry); + return ResponseResult.success(flowEntry.getEntryId()); + } + + /** + * 更新工作流对象数据。 + * + * @param flowEntryDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody FlowEntryDto flowEntryDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntry flowEntry = MyModelUtil.copyTo(flowEntryDto, FlowEntry.class); + ResponseResult verifyResult = this.doVerifyAndGet(flowEntry.getEntryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry originalFlowEntry = verifyResult.getData(); + if (ObjectUtil.notEqual(flowEntry.getProcessDefinitionKey(), originalFlowEntry.getProcessDefinitionKey())) { + if (originalFlowEntry.getStatus().equals(FlowEntryStatus.PUBLISHED)) { + errorMessage = "数据验证失败,当前流程为发布状态,流程标识不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowEntryService.existByProcessDefinitionKey(flowEntry.getProcessDefinitionKey())) { + errorMessage = "数据验证失败,该流程定义标识已存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + // 验证关联Id的数据合法性 + CallResult callResult = flowEntryService.verifyRelatedData(flowEntry, originalFlowEntry); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowEntryService.update(flowEntry, originalFlowEntry)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除工作流对象数据。 + * + * @param entryId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long entryId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(entryId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry originalFlowEntry = verifyResult.getData(); + if (originalFlowEntry.getStatus().equals(FlowEntryStatus.PUBLISHED)) { + errorMessage = "数据验证失败,当前流程为发布状态,不能删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowEntryService.remove(entryId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 发布工作流。 + * + * @param entryId 流程主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.PUBLISH) + @PostMapping("/publish") + public ResponseResult publish(@MyRequestBody(required = true) Long entryId) throws XMLStreamException { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry flowEntry = verifyResult.getData(); + if (StrUtil.isBlank(flowEntry.getBpmnXml())) { + errorMessage = "数据验证失败,该流程没有流程图不能被发布!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult taskInfoResult = this.verifyAndGetInitialTaskInfo(flowEntry); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + String taskInfo = taskInfoResult.getData() == null ? null : JSON.toJSONString(taskInfoResult.getData()); + flowEntryService.publish(flowEntry, taskInfo); + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的工作流列表。 + * + * @param flowEntryDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("flowEntry.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody FlowEntryDto flowEntryDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + FlowEntry flowEntryFilter = MyModelUtil.copyTo(flowEntryDtoFilter, FlowEntry.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowEntry.class); + List flowEntryList = flowEntryService.getFlowEntryListWithRelation(flowEntryFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(flowEntryList, FlowEntryVo.class)); + } + + /** + * 查看指定工作流对象详情。 + * + * @param entryId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("flowEntry.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long entryId) { + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry flowEntry = flowEntryService.getByIdWithRelation(entryId, MyRelationParam.full()); + if (flowEntry == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(flowEntry, FlowEntryVo.class); + } + + /** + * 列出指定流程的发布版本列表。 + * + * @param entryId 流程主键Id。 + * @return 应答结果对象,包含流程发布列表数据。 + */ + @SaCheckPermission("flowEntry.all") + @GetMapping("/listFlowEntryPublish") + public ResponseResult> listFlowEntryPublish(@RequestParam Long entryId) { + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + List flowEntryPublishList = flowEntryService.getFlowEntryPublishList(entryId); + return ResponseResult.success(MyModelUtil.copyCollectionTo(flowEntryPublishList, FlowEntryPublishVo.class)); + } + + /** + * 以字典形式返回全部FlowEntry数据集合。字典的键值为[entryId, procDefinitionName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject FlowEntryDto filter) { + List resultList = + flowEntryService.getFlowEntryList(MyModelUtil.copyTo(filter, FlowEntry.class), null); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, FlowEntry::getEntryId, FlowEntry::getProcessDefinitionName)); + } + + /** + * 获取所有流程分类和流程定义的列表。白名单接口。 + * + * @return 所有流程分类和流程定义的列表 + */ + @GetMapping("/listAll") + public ResponseResult listAll() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("flowEntryList", flowEntryService.getFlowEntryList(null, null)); + jsonObject.put("flowCategoryList", flowCategoryService.getFlowCategoryList(null, null)); + return ResponseResult.success(jsonObject); + } + + /** + * 白名单接口,根据流程Id,获取流程引擎需要的流程标识和流程名称。 + * + * @param entryId 流程Id。 + * @return 流程的部分数据。 + */ + @GetMapping("/viewDict") + public ResponseResult> viewDict(@RequestParam Long entryId) { + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntry flowEntry = verifyResult.getData(); + Map resultMap = new HashMap<>(2); + resultMap.put("processDefinitionKey", flowEntry.getProcessDefinitionKey()); + resultMap.put("processDefinitionName", flowEntry.getProcessDefinitionName()); + return ResponseResult.success(resultMap); + } + + /** + * 切换指定工作的发布主版本。 + * + * @param entryId 工作流主键Id。 + * @param newEntryPublishId 新的工作流发布主版本对象的主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateMainVersion") + public ResponseResult updateMainVersion( + @MyRequestBody(required = true) Long entryId, + @MyRequestBody(required = true) Long newEntryPublishId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(entryId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + FlowEntryPublish flowEntryPublish = flowEntryService.getFlowEntryPublishFromCache(newEntryPublishId); + if (flowEntryPublish == null) { + errorMessage = "数据验证失败,当前流程发布版本并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (ObjectUtil.notEqual(entryId, flowEntryPublish.getEntryId())) { + errorMessage = "数据验证失败,当前工作流并不包含该工作流发布版本数据,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (BooleanUtil.isTrue(flowEntryPublish.getMainVersion())) { + errorMessage = "数据验证失败,该版本已经为当前工作流的发布主版本,不能重复设置!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntryService.updateFlowEntryMainVersion(flowEntryService.getById(entryId), flowEntryPublish); + return ResponseResult.success(); + } + + /** + * 挂起工作流的指定发布版本。 + * + * @param entryPublishId 工作发布Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.SUSPEND) + @PostMapping("/suspendFlowEntryPublish") + public ResponseResult suspendFlowEntryPublish(@MyRequestBody(required = true) Long entryPublishId) { + String errorMessage; + FlowEntryPublish flowEntryPublish = flowEntryService.getFlowEntryPublishFromCache(entryPublishId); + if (flowEntryPublish == null) { + errorMessage = "数据验证失败,当前流程发布版本并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerifyAndGet(flowEntryPublish.getEntryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (BooleanUtil.isFalse(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布版本已处于挂起状态!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntryService.suspendFlowEntryPublish(flowEntryPublish); + return ResponseResult.success(); + } + + /** + * 激活工作流的指定发布版本。 + * + * @param entryPublishId 工作发布Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.RESUME) + @PostMapping("/activateFlowEntryPublish") + public ResponseResult activateFlowEntryPublish(@MyRequestBody(required = true) Long entryPublishId) { + String errorMessage; + FlowEntryPublish flowEntryPublish = flowEntryService.getFlowEntryPublishFromCache(entryPublishId); + if (flowEntryPublish == null) { + errorMessage = "数据验证失败,当前流程发布版本并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerifyAndGet(flowEntryPublish.getEntryId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (BooleanUtil.isTrue(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布版本已处于激活状态!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowEntryService.activateFlowEntryPublish(flowEntryPublish); + return ResponseResult.success(); + } + + private ResponseResult doVerifyAndGet(Long entryId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(entryId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + FlowEntry flowEntry = flowEntryService.getById(entryId); + if (flowEntry == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(flowEntry.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用并不存在该流程定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(flowEntry.getTenantId(), tokenData.getTenantId())) { + errorMessage = "数据验证失败,当前租户并不存在该流程定义!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(flowEntry); + } + + private ResponseResult verifyAndGetInitialTaskInfo(FlowEntry flowEntry) throws XMLStreamException { + String errorMessage; + BpmnModel bpmnModel = flowApiService.convertToBpmnModel(flowEntry.getBpmnXml()); + Process process = bpmnModel.getMainProcess(); + if (process == null) { + errorMessage = "数据验证失败,当前流程标识 [" + flowEntry.getProcessDefinitionKey() + "] 关联的流程模型并不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Collection elementList = process.getFlowElements(); + FlowElement startEvent = null; + // 这里我们只定位流程模型中的第二个节点。 + for (FlowElement flowElement : elementList) { + if (flowElement instanceof StartEvent) { + startEvent = flowElement; + break; + } + } + if (startEvent == null) { + errorMessage = "数据验证失败,当前流程图没有包含 [开始事件] 节点,请修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowElement firstTask = this.findFirstTask(elementList, startEvent); + if (firstTask == null) { + errorMessage = "数据验证失败,当前流程图没有包含 [开始事件] 节点没有任何连线,请修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfoVo; + if (firstTask instanceof UserTask) { + UserTask userTask = (UserTask) firstTask; + String formKey = userTask.getFormKey(); + if (StrUtil.isNotBlank(formKey)) { + taskInfoVo = JSON.parseObject(formKey, TaskInfoVo.class); + } else { + taskInfoVo = new TaskInfoVo(); + } + taskInfoVo.setAssignee(userTask.getAssignee()); + taskInfoVo.setTaskKey(userTask.getId()); + taskInfoVo.setTaskType(FlowTaskType.USER_TYPE); + Map> extensionMap = userTask.getExtensionElements(); + if (MapUtil.isNotEmpty(extensionMap)) { + taskInfoVo.setOperationList(flowTaskExtService.buildOperationListExtensionElement(extensionMap)); + taskInfoVo.setVariableList(flowTaskExtService.buildVariableListExtensionElement(extensionMap)); + } + } else { + taskInfoVo = new TaskInfoVo(); + taskInfoVo.setTaskType(FlowTaskType.OTHER_TYPE); + } + return ResponseResult.success(taskInfoVo); + } + + private FlowElement findFirstTask(Collection elementList, FlowElement startEvent) { + for (FlowElement flowElement : elementList) { + if (flowElement instanceof SequenceFlow) { + SequenceFlow sequenceFlow = (SequenceFlow) flowElement; + if (sequenceFlow.getSourceFlowElement().equals(startEvent)) { + return sequenceFlow.getTargetFlowElement(); + } + } + } + return null; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java new file mode 100644 index 00000000..371d37cc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowEntryVariableController.java @@ -0,0 +1,159 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.flow.vo.*; +import com.orangeforms.common.flow.dto.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import jakarta.validation.groups.Default; + +/** + * 工作流流程变量接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程变量接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowEntryVariable") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowEntryVariableController { + + @Autowired + private FlowEntryVariableService flowEntryVariableService; + + /** + * 新增流程变量数据。 + * + * @param flowEntryVariableDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"flowEntryVariableDto.variableId"}) + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody FlowEntryVariableDto flowEntryVariableDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryVariableDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntryVariable flowEntryVariable = MyModelUtil.copyTo(flowEntryVariableDto, FlowEntryVariable.class); + flowEntryVariable = flowEntryVariableService.saveNew(flowEntryVariable); + return ResponseResult.success(flowEntryVariable.getVariableId()); + } + + /** + * 更新流程变量数据。 + * + * @param flowEntryVariableDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody FlowEntryVariableDto flowEntryVariableDto) { + String errorMessage = MyCommonUtil.getModelValidationError(flowEntryVariableDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntryVariable flowEntryVariable = MyModelUtil.copyTo(flowEntryVariableDto, FlowEntryVariable.class); + FlowEntryVariable originalFlowEntryVariable = flowEntryVariableService.getById(flowEntryVariable.getVariableId()); + if (originalFlowEntryVariable == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!flowEntryVariableService.update(flowEntryVariable, originalFlowEntryVariable)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除流程变量数据。 + * + * @param variableId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("flowEntry.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long variableId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(variableId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + FlowEntryVariable originalFlowEntryVariable = flowEntryVariableService.getById(variableId); + if (originalFlowEntryVariable == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!flowEntryVariableService.remove(variableId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的流程变量列表。 + * + * @param flowEntryVariableDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("flowEntry.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody FlowEntryVariableDto flowEntryVariableDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + FlowEntryVariable flowEntryVariableFilter = MyModelUtil.copyTo(flowEntryVariableDtoFilter, FlowEntryVariable.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowEntryVariable.class); + List flowEntryVariableList = + flowEntryVariableService.getFlowEntryVariableListWithRelation(flowEntryVariableFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(flowEntryVariableList, FlowEntryVariableVo.class)); + } + + /** + * 查看指定流程变量对象详情。 + * + * @param variableId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("flowEntry.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long variableId) { + if (MyCommonUtil.existBlankArgument(variableId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + FlowEntryVariable flowEntryVariable = flowEntryVariableService.getByIdWithRelation(variableId, MyRelationParam.full()); + if (flowEntryVariable == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(flowEntryVariable, FlowEntryVariableVo.class); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java new file mode 100644 index 00000000..ffcc00b6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowMessageController.java @@ -0,0 +1,110 @@ +package com.orangeforms.common.flow.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.flow.model.constant.FlowMessageType; +import com.orangeforms.common.flow.model.FlowMessage; +import com.orangeforms.common.flow.service.FlowMessageService; +import com.orangeforms.common.flow.vo.FlowMessageVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 工作流消息接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流消息接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowMessage") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowMessageController { + + @Autowired + private FlowMessageService flowMessageService; + + /** + * 获取当前用户的未读消息总数。 + * NOTE:白名单接口。 + * + * @return 应答结果对象,包含当前用户的未读消息总数。 + */ + @GetMapping("/getMessageCount") + public ResponseResult getMessageCount() { + JSONObject resultData = new JSONObject(); + resultData.put("remindingMessageCount", flowMessageService.countRemindingMessageListByUser()); + resultData.put("copyMessageCount", flowMessageService.countCopyMessageByUser()); + return ResponseResult.success(resultData); + } + + /** + * 获取当前用户的催办消息列表。 + * 不仅仅包含,其中包括当前用户所属角色、部门和岗位的候选组催办消息。 + * NOTE:白名单接口。 + * + * @return 应答结果对象,包含查询结果集。 + */ + @PostMapping("/listRemindingTask") + public ResponseResult> listRemindingTask(@MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + List flowMessageList = flowMessageService.getRemindingMessageListByUser(); + return ResponseResult.success(MyPageUtil.makeResponseData(flowMessageList, FlowMessageVo.class)); + } + + /** + * 获取当前用户的抄送消息列表。 + * 不仅仅包含,其中包括当前用户所属角色、部门和岗位的候选组抄送消息。 + * NOTE:白名单接口。 + * + * @param read true表示已读,false表示未读。 + * @return 应答结果对象,包含查询结果集。 + */ + @PostMapping("/listCopyMessage") + public ResponseResult> listCopyMessage( + @MyRequestBody MyPageParam pageParam, @MyRequestBody Boolean read) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + } + List flowMessageList = flowMessageService.getCopyMessageListByUser(read); + return ResponseResult.success(MyPageUtil.makeResponseData(flowMessageList, FlowMessageVo.class)); + } + + /** + * 读取抄送消息,同时更新当前用户对指定抄送消息的读取状态。 + * + * @param messageId 消息Id。 + * @return 应答结果对象。 + */ + @PostMapping("/readCopyTask") + public ResponseResult readCopyTask(@MyRequestBody Long messageId) { + String errorMessage; + // 验证流程任务的合法性。 + FlowMessage flowMessage = flowMessageService.getById(messageId); + if (flowMessage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (flowMessage.getMessageType() != FlowMessageType.COPY_TYPE) { + errorMessage = "数据验证失败,当前消息不是抄送类型消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowMessageService.isCandidateIdentityOnMessage(messageId)) { + errorMessage = "数据验证失败,当前用户没有权限访问该消息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowMessageService.readCopyTask(messageId); + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java new file mode 100644 index 00000000..981fe6ac --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/controller/FlowOperationController.java @@ -0,0 +1,941 @@ +package com.orangeforms.common.flow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.util.FlowOperationHelper; +import com.orangeforms.common.flow.vo.FlowTaskCommentVo; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 工作流流程操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "工作流流程操作接口") +@Slf4j +@RestController +@RequestMapping("${common-flow.urlPrefix}/flowOperation") +@ConditionalOnProperty(name = "common-flow.operationEnabled", havingValue = "true") +public class FlowOperationController { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowTaskCommentService flowTaskCommentService; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowMessageService flowMessageService; + @Autowired + private FlowOperationHelper flowOperationHelper; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowMultiInstanceTransService flowMultiInstanceTransService; + + private static final String ACTIVE_MULTI_INST_TASK = "activeMultiInstanceTask"; + private static final String SHOW_NAME = "showName"; + private static final String INSTANCE_ID = "processInstanceId"; + + /** + * 获取开始节点之后的第一个任务节点的数据。 + * + * @param processDefinitionKey 流程标识。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewInitialTaskInfo") + public ResponseResult viewInitialTaskInfo(@RequestParam String processDefinitionKey) { + ResponseResult flowEntryResult = flowOperationHelper.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + String initTaskInfo = flowEntryPublish.getInitTaskInfo(); + TaskInfoVo taskInfo = StrUtil.isBlank(initTaskInfo) + ? null : JSON.parseObject(initTaskInfo, TaskInfoVo.class); + if (taskInfo != null) { + String loginName = TokenData.takeFromRequest().getLoginName(); + taskInfo.setAssignedMe(StrUtil.equalsAny( + taskInfo.getAssignee(), loginName, FlowConstant.START_USER_NAME_VAR)); + } + return ResponseResult.success(taskInfo); + } + + /** + * 获取流程运行时指定任务的信息。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @param processInstanceId 流程引擎的实例Id。 + * @param taskId 流程引擎的任务Id。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewRuntimeTaskInfo") + public ResponseResult viewRuntimeTaskInfo( + @RequestParam String processDefinitionId, + @RequestParam String processInstanceId, + @RequestParam String taskId) { + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfoVo = taskInfoResult.getData(); + FlowTaskExt flowTaskExt = + flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, taskInfoVo.getTaskKey()); + if (flowTaskExt != null) { + if (StrUtil.isNotBlank(flowTaskExt.getOperationListJson())) { + taskInfoVo.setOperationList(JSON.parseArray(flowTaskExt.getOperationListJson(), JSONObject.class)); + } + if (StrUtil.isNotBlank(flowTaskExt.getVariableListJson())) { + taskInfoVo.setVariableList(JSON.parseArray(flowTaskExt.getVariableListJson(), JSONObject.class)); + } + } + return ResponseResult.success(taskInfoVo); + } + + /** + * 获取流程运行时指定任务的信息。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @param processInstanceId 流程引擎的实例Id。 + * @param taskId 流程引擎的任务Id。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewHistoricTaskInfo") + public ResponseResult viewHistoricTaskInfo( + @RequestParam String processDefinitionId, + @RequestParam String processInstanceId, + @RequestParam String taskId) { + String errorMessage; + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + String loginName = TokenData.takeFromRequest().getLoginName(); + if (!StrUtil.equals(taskInstance.getAssignee(), loginName)) { + errorMessage = "数据验证失败,当前用户不是指派人!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfoVo = JSON.parseObject(taskInstance.getFormKey(), TaskInfoVo.class); + FlowTaskExt flowTaskExt = + flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, taskInstance.getTaskDefinitionKey()); + if (flowTaskExt != null) { + if (StrUtil.isNotBlank(flowTaskExt.getOperationListJson())) { + taskInfoVo.setOperationList(JSON.parseArray(flowTaskExt.getOperationListJson(), JSONObject.class)); + } + if (StrUtil.isNotBlank(flowTaskExt.getVariableListJson())) { + taskInfoVo.setVariableList(JSON.parseArray(flowTaskExt.getVariableListJson(), JSONObject.class)); + } + } + return ResponseResult.success(taskInfoVo); + } + + /** + * 获取第一个提交表单数据的任务信息。 + * + * @param processInstanceId 流程实例Id。 + * @return 任务节点的自定义对象数据。 + */ + @GetMapping("/viewInitialHistoricTaskInfo") + public ResponseResult viewInitialHistoricTaskInfo(@RequestParam String processInstanceId) { + String errorMessage; + List taskCommentList = + flowTaskCommentService.getFlowTaskCommentList(processInstanceId); + if (CollUtil.isEmpty(taskCommentList)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + FlowTaskComment taskComment = taskCommentList.get(0); + HistoricTaskInstance task = flowApiService.getHistoricTaskInstance(processInstanceId, taskComment.getTaskId()); + if (StrUtil.isBlank(task.getFormKey())) { + errorMessage = "数据验证失败,指定任务的formKey属性不存在,请重新修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfo = JSON.parseObject(task.getFormKey(), TaskInfoVo.class); + taskInfo.setTaskKey(task.getTaskDefinitionKey()); + return ResponseResult.success(taskInfo); + } + + /** + * 获取任务的用户信息列表。 + * + * @param processDefinitionId 流程定义Id。 + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param historic 是否为历史任务。 + * @return 任务相关的用户信息列表。 + */ + @DisableDataFilter + @GetMapping("/viewTaskUserInfo") + public ResponseResult> viewTaskUserInfo( + @RequestParam String processDefinitionId, + @RequestParam String processInstanceId, + @RequestParam String taskId, + @RequestParam Boolean historic) { + TaskInfo taskInfo; + HistoricTaskInstance hisotricTask; + if (BooleanUtil.isFalse(historic)) { + taskInfo = flowApiService.getTaskById(taskId); + if (taskInfo == null) { + hisotricTask = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + taskInfo = hisotricTask; + historic = true; + } + } else { + hisotricTask = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + taskInfo = hisotricTask; + } + if (taskInfo == null) { + String errorMessage = "数据验证失败,任务Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + String taskKey = taskInfo.getTaskDefinitionKey(); + FlowTaskExt taskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, taskKey); + boolean isMultiInstanceTask = flowApiService.isMultiInstanceTask(taskInfo.getProcessDefinitionId(), taskKey); + List resultUserInfoList = + flowTaskExtService.getCandidateUserInfoList(processInstanceId, taskExt, taskInfo, isMultiInstanceTask, historic); + if (BooleanUtil.isTrue(historic) || isMultiInstanceTask) { + List taskCommentList = buildApprovedFlowTaskCommentList(taskInfo, isMultiInstanceTask); + Map resultUserInfoMap = + resultUserInfoList.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + for (FlowTaskComment taskComment : taskCommentList) { + FlowUserInfoVo flowUserInfoVo = resultUserInfoMap.get(taskComment.getCreateLoginName()); + if (flowUserInfoVo != null) { + flowUserInfoVo.setLastApprovalTime(taskComment.getCreateTime()); + } + } + } + return ResponseResult.success(resultUserInfoList); + } + + /** + * 获取多实例会签任务的指派人列表。 + * NOTE: 白名单接口。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 多实例任务的上一级任务Id。 + * @return 应答结果,指定会签任务的指派人列表。 + */ + @GetMapping("/listMultiSignAssignees") + public ResponseResult> listMultiSignAssignees( + @RequestParam String processInstanceId, @RequestParam String taskId) { + ResponseResult verifyResult = this.doVerifyMultiSign(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + Task activeMultiInstanceTask = + verifyResult.getData().getObject(ACTIVE_MULTI_INST_TASK, Task.class); + String multiInstanceExecId = flowApiService.getExecutionVariableStringWithSafe( + activeMultiInstanceTask.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + List commentList = + flowTaskCommentService.getFlowTaskCommentListByMultiInstanceExecId(multiInstanceExecId); + List assigneeList = StrUtil.split(trans.getAssigneeList(), ","); + Set approvedAssigneeSet = commentList.stream() + .map(FlowTaskComment::getCreateLoginName).collect(Collectors.toSet()); + List resultList = new LinkedList<>(); + Map usernameMap = + flowCustomExtFactory.getFlowIdentityExtHelper().mapUserShowNameByLoginName(new HashSet<>(assigneeList)); + for (String assignee : assigneeList) { + JSONObject resultData = new JSONObject(); + resultData.put("assignee", assignee); + resultData.put(SHOW_NAME, usernameMap.get(assignee)); + resultData.put("approved", approvedAssigneeSet.contains(assignee)); + resultList.add(resultData); + } + return ResponseResult.success(resultList); + } + + /** + * 提交多实例加签或减签。 + * NOTE: 白名单接口。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 多实例任务的上一级任务Id。 + * @param newAssignees 加签减签人列表,多个指派人之间逗号分隔。 + * @param isAdd 是否为加签,如果没有该参数,为了保持兼容性,缺省值为true。 + * @return 应答结果。 + */ + @PostMapping("/submitConsign") + public ResponseResult submitConsign( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String newAssignees, + @MyRequestBody Boolean isAdd) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyMultiSign(processInstanceId, taskId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + HistoricTaskInstance taskInstance = + verifyResult.getData().getObject("taskInstance", HistoricTaskInstance.class); + Task activeMultiInstanceTask = + verifyResult.getData().getObject(ACTIVE_MULTI_INST_TASK, Task.class); + String multiInstanceExecId = flowApiService.getExecutionVariableStringWithSafe( + activeMultiInstanceTask.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + JSONArray assigneeArray = JSON.parseArray(newAssignees); + if (isAdd == null) { + isAdd = true; + } + if (!isAdd) { + List commentList = + flowTaskCommentService.getFlowTaskCommentListByMultiInstanceExecId(multiInstanceExecId); + if (CollUtil.isNotEmpty(commentList)) { + Set approvedAssigneeSet = commentList.stream() + .map(FlowTaskComment::getCreateLoginName).collect(Collectors.toSet()); + String loginName = this.findExistAssignee(approvedAssigneeSet, assigneeArray); + if (loginName != null) { + errorMessage = "数据验证失败,用户 [" + loginName + "] 已经审批,不能减签该用户!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + } else { + // 避免同一人被重复加签。 + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + Set assigneeSet = new HashSet<>(StrUtil.split(trans.getAssigneeList(), ",")); + String loginName = this.findExistAssignee(assigneeSet, assigneeArray); + if (loginName != null) { + errorMessage = "数据验证失败,用户 [" + loginName + "] 已经是会签人,不能重复指定!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + try { + flowApiService.submitConsign(taskInstance, activeMultiInstanceTask, newAssignees, isAdd); + } catch (FlowOperationException e) { + errorMessage = e.getMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 返回当前用户待办的任务列表。 + * + * @param processDefinitionKey 流程标识。 + * @param processDefinitionName 流程定义名 (模糊查询)。 + * @param taskName 任务名称 (模糊查询)。 + * @param pageParam 分页对象。 + * @return 返回当前用户待办的任务列表。如果指定流程标识,则仅返回该流程的待办任务列表。 + */ + @DisableDataFilter + @PostMapping("/listRuntimeTask") + public ResponseResult> listRuntimeTask( + @MyRequestBody String processDefinitionKey, + @MyRequestBody String processDefinitionName, + @MyRequestBody String taskName, + @MyRequestBody(required = true) MyPageParam pageParam) { + String username = TokenData.takeFromRequest().getLoginName(); + MyPageData pageData = flowApiService.getTaskListByUserName( + username, processDefinitionKey, processDefinitionName, taskName, pageParam); + List flowTaskVoList = flowApiService.convertToFlowTaskList(pageData.getDataList()); + return ResponseResult.success(MyPageUtil.makeResponseData(flowTaskVoList, pageData.getTotalCount())); + } + + /** + * 返回当前用户待办的任务数量。 + * + * @return 返回当前用户待办的任务数量。 + */ + @PostMapping("/countRuntimeTask") + public ResponseResult countRuntimeTask() { + String username = TokenData.takeFromRequest().getLoginName(); + long totalCount = flowApiService.getTaskCountByUserName(username); + return ResponseResult.success(totalCount); + } + + /** + * 主动驳回当前的待办任务到开始节点,只用当前待办任务的指派人或者候选者才能完成该操作。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 待办任务Id。 + * @param taskComment 驳回备注。 + * @return 操作应答结果。 + */ + @PostMapping("/rejectToStartUserTask") + public ResponseResult rejectToStartUserTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String taskComment) { + ResponseResult taskResult = + flowOperationHelper.verifySubmitAndGetTask(processInstanceId, taskId, null); + if (!taskResult.isSuccess()) { + return ResponseResult.errorFrom(taskResult); + } + FlowTaskComment firstTaskComment = flowTaskCommentService.getFirstFlowTaskComment(processInstanceId); + CallResult result = flowApiService.backToRuntimeTask( + taskResult.getData(), firstTaskComment.getTaskKey(), true, taskComment); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 主动驳回当前的待办任务,只用当前待办任务的指派人或者候选者才能完成该操作。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 待办任务Id。 + * @param taskComment 驳回备注。 + * @return 操作应答结果。 + */ + @PostMapping("/rejectRuntimeTask") + public ResponseResult rejectRuntimeTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String taskComment) { + String errorMessage; + ResponseResult taskResult = + flowOperationHelper.verifySubmitAndGetTask(processInstanceId, taskId, null); + if (!taskResult.isSuccess()) { + return ResponseResult.errorFrom(taskResult); + } + CallResult result = flowApiService.backToRuntimeTask(taskResult.getData(), null, true, taskComment); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 撤回当前用户提交的,但是尚未被审批的待办任务。只有已办任务的指派人才能完成该操作。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 待撤回的已办任务Id。 + * @param taskComment 撤回备注。 + * @return 操作应答结果。 + */ + @PostMapping("/revokeHistoricTask") + public ResponseResult revokeHistoricTask( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String taskId, + @MyRequestBody(required = true) String taskComment) { + String errorMessage; + if (!flowApiService.existActiveProcessInstance(processInstanceId)) { + errorMessage = "数据验证失败,当前流程实例已经结束,不能执行撤回!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (taskInstance == null) { + errorMessage = "数据验证失败,当前任务不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(taskInstance.getAssignee(), TokenData.takeFromRequest().getLoginName())) { + errorMessage = "数据验证失败,任务指派人与当前用户不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowTaskComment latestComment = flowTaskCommentService.getLatestFlowTaskComment(processInstanceId); + if (latestComment == null) { + errorMessage = "数据验证失败,当前实例没有任何审批提交记录!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!latestComment.getTaskId().equals(taskId)) { + errorMessage = "数据验证失败,当前审批任务已被办理,不能撤回!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + List activeTaskList = flowApiService.getProcessInstanceActiveTaskList(processInstanceId); + if (CollUtil.isEmpty(activeTaskList)) { + errorMessage = "数据验证失败,当前流程没有任何待办任务!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (latestComment.getApprovalType().equals(FlowApprovalType.TRANSFER)) { + if (activeTaskList.size() > 1) { + errorMessage = "数据验证失败,转办任务数量不能多于1个!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 如果是转办任务,无需节点跳转,将指派人改为当前用户即可。 + Task task = activeTaskList.get(0); + task.setAssignee(TokenData.takeFromRequest().getLoginName()); + } else { + CallResult result = + flowApiService.backToRuntimeTask(activeTaskList.get(0), null, false, taskComment); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + } + return ResponseResult.success(); + } + + /** + * 获取当前流程任务的审批列表。 + * + * @param processInstanceId 当前运行时的流程实例Id。 + * @return 当前流程实例的详情数据。 + */ + @GetMapping("/listFlowTaskComment") + public ResponseResult> listFlowTaskComment(@RequestParam String processInstanceId) { + List flowTaskCommentList = + flowTaskCommentService.getFlowTaskCommentList(processInstanceId); + List resultList = MyModelUtil.copyCollectionTo(flowTaskCommentList, FlowTaskCommentVo.class); + return ResponseResult.success(resultList); + } + + /** + * 获取指定流程定义的流程图。 + * + * @param processDefinitionId 流程定义Id。 + * @return 流程图。 + */ + @GetMapping("/viewProcessBpmn") + public ResponseResult viewProcessBpmn(@RequestParam String processDefinitionId) throws IOException { + BpmnXMLConverter converter = new BpmnXMLConverter(); + BpmnModel bpmnModel = flowApiService.getBpmnModelByDefinitionId(processDefinitionId); + byte[] xmlBytes = converter.convertToXML(bpmnModel); + InputStream in = new ByteArrayInputStream(xmlBytes); + return ResponseResult.success(StreamUtils.copyToString(in, StandardCharsets.UTF_8)); + } + + /** + * 获取流程图高亮数据。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程图高亮数据。 + */ + @GetMapping("/viewHighlightFlowData") + public ResponseResult viewHighlightFlowData(@RequestParam String processInstanceId) { + List activityInstanceList = + flowApiService.getHistoricActivityInstanceList(processInstanceId); + Set finishedTaskSet = activityInstanceList.stream() + .filter(s -> !StrUtil.equals(s.getActivityType(), "sequenceFlow")) + .map(HistoricActivityInstance::getActivityId).collect(Collectors.toSet()); + Set finishedSequenceFlowSet = activityInstanceList.stream() + .filter(s -> StrUtil.equals(s.getActivityType(), "sequenceFlow")) + .map(HistoricActivityInstance::getActivityId).collect(Collectors.toSet()); + //获取流程实例当前正在待办的节点 + List unfinishedInstanceList = + flowApiService.getHistoricUnfinishedInstanceList(processInstanceId); + Set unfinishedTaskSet = new LinkedHashSet<>(); + for (HistoricActivityInstance unfinishedActivity : unfinishedInstanceList) { + unfinishedTaskSet.add(unfinishedActivity.getActivityId()); + } + JSONObject jsonData = new JSONObject(); + jsonData.put("finishedTaskSet", finishedTaskSet); + jsonData.put("finishedSequenceFlowSet", finishedSequenceFlowSet); + jsonData.put("unfinishedTaskSet", unfinishedTaskSet); + return ResponseResult.success(jsonData); + } + + /** + * 获取当前用户的已办理的审批任务列表。 + * + * @param processDefinitionName 流程名。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 查询结果应答。 + */ + @DisableDataFilter + @PostMapping("/listHistoricTask") + public ResponseResult>> listHistoricTask( + @MyRequestBody String processDefinitionName, + @MyRequestBody String beginDate, + @MyRequestBody String endDate, + @MyRequestBody(required = true) MyPageParam pageParam) throws ParseException { + MyPageData pageData = + flowApiService.getHistoricTaskInstanceFinishedList(processDefinitionName, beginDate, endDate, pageParam); + List> resultList = new LinkedList<>(); + pageData.getDataList().forEach(instance -> resultList.add(BeanUtil.beanToMap(instance))); + List taskInstanceList = pageData.getDataList(); + if (CollUtil.isNotEmpty(taskInstanceList)) { + Set instanceIdSet = taskInstanceList.stream() + .map(HistoricTaskInstance::getProcessInstanceId).collect(Collectors.toSet()); + List instanceList = flowApiService.getHistoricProcessInstanceList(instanceIdSet); + Set loginNameSet = instanceList.stream() + .map(HistoricProcessInstance::getStartUserId).collect(Collectors.toSet()); + List userInfoList = flowCustomExtFactory + .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); + Map userInfoMap = + userInfoList.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + Map instanceMap = + instanceList.stream().collect(Collectors.toMap(HistoricProcessInstance::getId, c -> c)); + List workOrderList = + flowWorkOrderService.getInList(INSTANCE_ID, instanceIdSet); + Map workOrderMap = + workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + resultList.forEach(result -> { + String instanceId = result.get(INSTANCE_ID).toString(); + HistoricProcessInstance instance = instanceMap.get(instanceId); + result.put("processDefinitionKey", instance.getProcessDefinitionKey()); + result.put("processDefinitionName", instance.getProcessDefinitionName()); + result.put("startUser", instance.getStartUserId()); + FlowUserInfoVo userInfo = userInfoMap.get(instance.getStartUserId()); + result.put(SHOW_NAME, userInfo.getShowName()); + result.put("headImageUrl", userInfo.getHeadImageUrl()); + result.put("businessKey", instance.getBusinessKey()); + FlowWorkOrder flowWorkOrder = workOrderMap.get(instanceId); + if (flowWorkOrder != null) { + result.put("workOrderCode", flowWorkOrder.getWorkOrderCode()); + } + }); + Set taskIdSet = + taskInstanceList.stream().map(HistoricTaskInstance::getId).collect(Collectors.toSet()); + List commentList = flowTaskCommentService.getFlowTaskCommentListByTaskIds(taskIdSet); + Map> commentMap = + commentList.stream().collect(Collectors.groupingBy(FlowTaskComment::getTaskId)); + resultList.forEach(result -> { + List comments = commentMap.get(result.get("id").toString()); + if (CollUtil.isNotEmpty(comments)) { + result.put("approvalType", comments.get(0).getApprovalType()); + comments.remove(0); + } + }); + } + return ResponseResult.success(MyPageUtil.makeResponseData(resultList, pageData.getTotalCount())); + } + + /** + * 根据输入参数查询,当前用户的历史流程数据。 + * + * @param processDefinitionName 流程名。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 查询结果应答。 + */ + @DisableDataFilter + @PostMapping("/listHistoricProcessInstance") + public ResponseResult>> listHistoricProcessInstance( + @MyRequestBody String processDefinitionName, + @MyRequestBody String beginDate, + @MyRequestBody String endDate, + @MyRequestBody(required = true) MyPageParam pageParam) throws ParseException { + String loginName = TokenData.takeFromRequest().getLoginName(); + MyPageData pageData = flowApiService.getHistoricProcessInstanceList( + null, processDefinitionName, loginName, beginDate, endDate, pageParam, true); + Set loginNameSet = pageData.getDataList().stream() + .map(HistoricProcessInstance::getStartUserId).collect(Collectors.toSet()); + List userInfoList = flowCustomExtFactory + .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); + if (CollUtil.isEmpty(userInfoList)) { + userInfoList = new LinkedList<>(); + } + Map userInfoMap = + userInfoList.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + Set instanceIdSet = pageData.getDataList().stream() + .map(HistoricProcessInstance::getId).collect(Collectors.toSet()); + List workOrderList = + flowWorkOrderService.getInList(INSTANCE_ID, instanceIdSet); + Map workOrderMap = + workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + List> resultList = new LinkedList<>(); + pageData.getDataList().forEach(instance -> { + Map data = BeanUtil.beanToMap(instance); + FlowUserInfoVo userInfo = userInfoMap.get(instance.getStartUserId()); + if (userInfo != null) { + data.put(SHOW_NAME, userInfo.getShowName()); + data.put("headImageUrl", userInfo.getHeadImageUrl()); + } + FlowWorkOrder workOrder = workOrderMap.get(instance.getId()); + if (workOrder != null) { + data.put("workOrderCode", workOrder.getWorkOrderCode()); + data.put("flowStatus", workOrder.getFlowStatus()); + } + resultList.add(data); + }); + return ResponseResult.success(MyPageUtil.makeResponseData(resultList, pageData.getTotalCount())); + } + + /** + * 根据输入参数查询,所有历史流程数据。 + * + * @param processDefinitionName 流程名。 + * @param startUser 流程发起用户。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 查询结果。 + */ + @PostMapping("/listAllHistoricProcessInstance") + public ResponseResult>> listAllHistoricProcessInstance( + @MyRequestBody String processDefinitionName, + @MyRequestBody String startUser, + @MyRequestBody String beginDate, + @MyRequestBody String endDate, + @MyRequestBody(required = true) MyPageParam pageParam) throws ParseException { + MyPageData pageData = flowApiService.getHistoricProcessInstanceList( + null, processDefinitionName, startUser, beginDate, endDate, pageParam, false); + List> resultList = new LinkedList<>(); + pageData.getDataList().forEach(instance -> resultList.add(BeanUtil.beanToMap(instance))); + List unfinishedProcessInstanceIds = pageData.getDataList().stream() + .filter(c -> c.getEndTime() == null) + .map(HistoricProcessInstance::getId) + .collect(Collectors.toList()); + MyPageData> pageResultData = + MyPageUtil.makeResponseData(resultList, pageData.getTotalCount()); + if (CollUtil.isEmpty(unfinishedProcessInstanceIds)) { + return ResponseResult.success(pageResultData); + } + Set processInstanceIds = pageData.getDataList().stream() + .map(HistoricProcessInstance::getId).collect(Collectors.toSet()); + List taskList = flowApiService.getTaskListByProcessInstanceIds(unfinishedProcessInstanceIds); + Map> taskMap = + taskList.stream().collect(Collectors.groupingBy(Task::getProcessInstanceId)); + for (Map result : resultList) { + String processInstanceId = result.get(INSTANCE_ID).toString(); + List instanceTaskList = taskMap.get(processInstanceId); + if (instanceTaskList != null) { + JSONArray taskArray = new JSONArray(); + for (Task task : instanceTaskList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("taskId", task.getId()); + jsonObject.put("taskName", task.getName()); + jsonObject.put("taskKey", task.getTaskDefinitionKey()); + jsonObject.put("assignee", task.getAssignee()); + taskArray.add(jsonObject); + } + result.put("runtimeTaskInfoList", taskArray); + } + } + return ResponseResult.success(pageResultData); + } + + /** + * 催办工单,只有流程发起人才可以催办工单。 + * 催办场景必须要取消数据权限过滤,因为流程的指派很可能是跨越部门的。 + * 既然被指派和催办了,这里就应该禁用工单表的数据权限过滤约束。 + * 如果您的系统没有支持数据权限过滤,DisableDataFilter不会有任何影响,建议保留。 + * + * @param workOrderId 工单Id。 + * @return 应答结果。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.REMIND_TASK) + @PostMapping("/remindRuntimeTask") + public ResponseResult remindRuntimeTask(@MyRequestBody(required = true) Long workOrderId) { + FlowWorkOrder flowWorkOrder = flowWorkOrderService.getById(workOrderId); + if (flowWorkOrder == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMessage; + if (!flowWorkOrder.getCreateUserId().equals(TokenData.takeFromRequest().getUserId())) { + errorMessage = "数据验证失败,只有流程发起人才能催办工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.FINISHED) + || flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.CANCELLED) + || flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.STOPPED)) { + errorMessage = "数据验证失败,已经结束的流程,不能催办工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + errorMessage = "数据验证失败,流程草稿不能催办工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + flowMessageService.saveNewRemindMessage(flowWorkOrder); + return ResponseResult.success(); + } + + /** + * 取消工作流工单,仅当没有进入任何审批流程之前,才可以取消工单。 + * + * @param workOrderId 工单Id。 + * @param cancelReason 取消原因。 + * @return 应答结果。 + */ + @OperationLog(type = SysOperationLogType.CANCEL_FLOW) + @DisableDataFilter + @PostMapping("/cancelWorkOrder") + public ResponseResult cancelWorkOrder( + @MyRequestBody(required = true) Long workOrderId, + @MyRequestBody(required = true) String cancelReason) { + FlowWorkOrder flowWorkOrder = flowWorkOrderService.getById(workOrderId); + if (flowWorkOrder == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMessage; + if (!flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.SUBMITTED) + && !flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + errorMessage = "数据验证失败,当前流程已经进入审批状态,不能撤销工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowWorkOrder.getCreateUserId().equals(TokenData.takeFromRequest().getUserId())) { + errorMessage = "数据验证失败,当前用户不是工单所有者,不能撤销工单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult result; + // 草稿工单直接删除当前工单。 + if (flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + result = flowWorkOrderService.removeDraft(flowWorkOrder); + } else { + result = flowApiService.stopProcessInstance( + flowWorkOrder.getProcessInstanceId(), cancelReason, true); + } + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 获取指定流程定义Id的所有用户任务数据列表。 + * + * @param processDefinitionId 流程定义Id。 + * @return 查询结果。 + */ + @GetMapping("/listAllUserTask") + public ResponseResult> listAllUserTask(@RequestParam String processDefinitionId) { + Map taskMap = flowApiService.getAllUserTaskMap(processDefinitionId); + List resultList = new LinkedList<>(); + for (UserTask t : taskMap.values()) { + JSONObject data = new JSONObject(); + data.put("id", t.getId()); + data.put("name", t.getName()); + resultList.add(data); + } + return ResponseResult.success(resultList); + } + + /** + * 终止流程实例,将任务从当前节点直接流转到主流程的结束事件。 + * + * @param processInstanceId 流程实例Id。 + * @param stopReason 停止原因。 + * @return 执行结果应答。 + */ + @SaCheckPermission("flowOperation.all") + @OperationLog(type = SysOperationLogType.STOP_FLOW) + @DisableDataFilter + @PostMapping("/stopProcessInstance") + public ResponseResult stopProcessInstance( + @MyRequestBody(required = true) String processInstanceId, + @MyRequestBody(required = true) String stopReason) { + CallResult result = flowApiService.stopProcessInstance(processInstanceId, stopReason, false); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + return ResponseResult.success(); + } + + /** + * 删除流程实例。 + * + * @param processInstanceId 流程实例Id。 + * @return 执行结果应答。 + */ + @SaCheckPermission("flowOperation.all") + @OperationLog(type = SysOperationLogType.DELETE_FLOW) + @PostMapping("/deleteProcessInstance") + public ResponseResult deleteProcessInstance(@MyRequestBody(required = true) String processInstanceId) { + flowApiService.deleteProcessInstance(processInstanceId); + return ResponseResult.success(); + } + + private List buildApprovedFlowTaskCommentList(TaskInfo taskInfo, boolean isMultiInstanceTask) { + List taskCommentList; + if (isMultiInstanceTask) { + String multiInstanceExecId; + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getByExecutionId(taskInfo.getExecutionId(), taskInfo.getId()); + if (trans != null) { + multiInstanceExecId = trans.getMultiInstanceExecId(); + } else { + multiInstanceExecId = flowApiService.getExecutionVariableStringWithSafe( + taskInfo.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + } + taskCommentList = flowTaskCommentService.getFlowTaskCommentListByMultiInstanceExecId(multiInstanceExecId); + } else { + taskCommentList = flowTaskCommentService.getFlowTaskCommentListByExecutionId( + taskInfo.getProcessInstanceId(), taskInfo.getId(), taskInfo.getExecutionId()); + } + return taskCommentList; + } + + private ResponseResult doVerifyMultiSign(String processInstanceId, String taskId) { + String errorMessage; + if (!flowApiService.existActiveProcessInstance(processInstanceId)) { + errorMessage = "数据验证失败,当前流程实例已经结束,不能执行加签!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (taskInstance == null) { + errorMessage = "数据验证失败,当前任务不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String loginName = TokenData.takeFromRequest().getLoginName(); + if (!StrUtil.equals(taskInstance.getAssignee(), loginName)) { + errorMessage = "数据验证失败,任务指派人与当前用户不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + List activeTaskList = flowApiService.getProcessInstanceActiveTaskList(processInstanceId); + Task activeMultiInstanceTask = null; + Map userTaskMap = flowApiService.getAllUserTaskMap(taskInstance.getProcessDefinitionId()); + for (Task activeTask : activeTaskList) { + UserTask userTask = userTaskMap.get(activeTask.getTaskDefinitionKey()); + if (!userTask.hasMultiInstanceLoopCharacteristics()) { + errorMessage = "数据验证失败,指定加签任务不存在或已审批完毕!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String startTaskId = flowApiService.getTaskVariableStringWithSafe( + activeTask.getId(), FlowConstant.MULTI_SIGN_START_TASK_VAR); + if (StrUtil.equals(startTaskId, taskId)) { + activeMultiInstanceTask = activeTask; + break; + } + } + if (activeMultiInstanceTask == null) { + errorMessage = "数据验证失败,指定加签任务不存在或已审批完毕!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + JSONObject resultData = new JSONObject(); + resultData.put("taskInstance", taskInstance); + resultData.put(ACTIVE_MULTI_INST_TASK, activeMultiInstanceTask); + return ResponseResult.success(resultData); + } + + private String findExistAssignee(Set assigneeSet, JSONArray assigneeArray) { + for (int i = 0; i < assigneeArray.size(); i++) { + String loginName = assigneeArray.getString(i); + if (assigneeSet.contains(loginName)) { + return loginName; + } + } + return null; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java new file mode 100644 index 00000000..7cd964f6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowCategoryMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowCategory; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * FlowCategory数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowCategoryMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowCategoryFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowCategoryList( + @Param("flowCategoryFilter") FlowCategory flowCategoryFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java new file mode 100644 index 00000000..3e4154a8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntry; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * FlowEntry数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowEntryFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowEntryList( + @Param("flowEntryFilter") FlowEntry flowEntryFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java new file mode 100644 index 00000000..233c5531 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntryPublish; + +/** + * 数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryPublishMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java new file mode 100644 index 00000000..76de0460 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryPublishVariableMapper.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntryPublishVariable; + +import java.util.List; + +/** + * 数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryPublishVariableMapper extends BaseDaoMapper { + + /** + * 批量插入流程发布的变量列表。 + * + * @param entryPublishVariableList 流程发布的变量列表。 + */ + void insertList(List entryPublishVariableList); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java new file mode 100644 index 00000000..c7c133bb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowEntryVariableMapper.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowEntryVariable; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 流程变量数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryVariableMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowEntryVariableFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowEntryVariableList( + @Param("flowEntryVariableFilter") FlowEntryVariable flowEntryVariableFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java new file mode 100644 index 00000000..c37279f2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageCandidateIdentityMapper.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMessageCandidateIdentity; +import org.apache.ibatis.annotations.Param; + +/** + * 流程任务消息的候选身份数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageCandidateIdentityMapper extends BaseDaoMapper { + + /** + * 删除指定流程实例的消息关联数据。 + * + * @param processInstanceId 流程实例Id。 + */ + void deleteByProcessInstanceId(@Param("processInstanceId") String processInstanceId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java new file mode 100644 index 00000000..bc635b07 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageIdentityOperationMapper.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMessageIdentityOperation; +import org.apache.ibatis.annotations.Param; + +/** + * 流程任务消息所属用户的操作数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageIdentityOperationMapper extends BaseDaoMapper { + + /** + * 删除指定流程实例的消息关联数据。 + * + * @param processInstanceId 流程实例Id。 + */ + void deleteByProcessInstanceId(@Param("processInstanceId") String processInstanceId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java new file mode 100644 index 00000000..b34474ae --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMessageMapper.java @@ -0,0 +1,79 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMessage; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + * 工作流消息数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageMapper extends BaseDaoMapper { + + /** + * 获取指定用户和身份分组Id集合的催办消息列表。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户的登录名。与流程任务的assignee精确匹配。 + * @param groupIdSet 用户身份分组Id集合。 + * @return 查询后的催办消息列表。 + */ + List getRemindingMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet); + + /** + * 获取指定用户和身份分组Id集合的抄送消息列表。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户登录名。 + * @param groupIdSet 用户身份分组Id集合。 + * @param read true表示已读,false表示未读。 + * @return 查询后的抄送消息列表。 + */ + List getCopyMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet, + @Param("read") Boolean read); + + /** + * 计算当前用户催办消息的数量。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户登录名。 + * @param groupIdSet 用户身份分组Id集合。 + * @return 数据数量。 + */ + int countRemindingMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet); + + /** + * 计算当前用户未读抄送消息的数量。 + * + * @param tenantId 租户Id。 + * @param appCode 应用编码。 + * @param loginName 用户登录名。 + * @param groupIdSet 用户身份分组Id集合。 + * @return 数据数量 + */ + int countCopyMessageListByUser( + @Param("tenantId") Long tenantId, + @Param("appCode") String appCode, + @Param("loginName") String loginName, + @Param("groupIdSet") Set groupIdSet); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java new file mode 100644 index 00000000..131e9368 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowMultiInstanceTransMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowMultiInstanceTrans; + +/** + * 流程多实例任务执行流水访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMultiInstanceTransMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java new file mode 100644 index 00000000..5da2bf06 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskCommentMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowTaskComment; + +/** + * 流程任务批注数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskCommentMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java new file mode 100644 index 00000000..9145a5e2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowTaskExtMapper.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowTaskExt; + +import java.util.List; + +/** + * 流程任务扩展数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskExtMapper extends BaseDaoMapper { + + /** + * 批量插入流程任务扩展信息列表。 + * + * @param flowTaskExtList 流程任务扩展信息列表。 + */ + void insertList(List flowTaskExtList); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java new file mode 100644 index 00000000..b69fd718 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderExtMapper.java @@ -0,0 +1,14 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowWorkOrderExt; + +/** + * 工作流工单扩展数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowWorkOrderExtMapper extends BaseDaoMapper { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java new file mode 100644 index 00000000..fe270142 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/FlowWorkOrderMapper.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.flow.dao; + +import com.orangeforms.common.core.annotation.EnableDataPerm; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.*; + +/** + * 工作流工单表数据操作访问接口。 + * 如果当前系统支持数据权限过滤,当前用户必须要能看自己的工单数据,所以需要把EnableDataPerm + * 的mustIncludeUserRule参数设置为true,即便当前用户的数据权限中并不包含DataPermRuleType.TYPE_USER_ONLY, + * 数据过滤拦截组件也会自动补偿该类型的数据权限,以便当前用户可以看到自己发起的工单。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableDataPerm(mustIncludeUserRule = true) +public interface FlowWorkOrderMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param flowWorkOrderFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getFlowWorkOrderList( + @Param("flowWorkOrderFilter") FlowWorkOrder flowWorkOrderFilter, @Param("orderBy") String orderBy); + + /** + * 计算指定前缀工单编码的最大值。 + * + * @param prefix 工单编码前缀。 + * @return 该工单编码前缀的最大值。 + */ + @Select("SELECT MAX(work_order_code) FROM zz_flow_work_order WHERE work_order_code LIKE '${prefix}'") + String getMaxWorkOrderCodeByPrefix(@Param("prefix") String prefix); + + /** + * 根据工单编码查询指定工单,查询过程也会考虑逻辑删除的数据。 + * @param workOrderCode 工单编码。 + * @return 工单编码的流程工单数量。 + */ + @Select("SELECT COUNT(*) FROM zz_flow_work_order WHERE work_order_code = #{workOrderCode}") + int getCountByWorkOrderCode(@Param("workOrderCode") String workOrderCode); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml new file mode 100644 index 00000000..65460911 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowCategoryMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_category.tenant_id IS NULL + + + AND zz_flow_category.tenant_id = #{flowCategoryFilter.tenantId} + + + AND zz_flow_category.app_code IS NULL + + + AND zz_flow_category.app_code = #{flowCategoryFilter.appCode} + + + AND zz_flow_category.name = #{flowCategoryFilter.name} + + + AND zz_flow_category.code = #{flowCategoryFilter.code} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml new file mode 100644 index 00000000..78351d5d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_entry.tenant_id IS NULL + + + AND zz_flow_entry.tenant_id = #{flowEntryFilter.tenantId} + + + AND zz_flow_entry.app_code IS NULL + + + AND zz_flow_entry.app_code = #{flowEntryFilter.appCode} + + + AND zz_flow_entry.process_definition_name = #{flowEntryFilter.processDefinitionName} + + + AND zz_flow_entry.process_definition_key = #{flowEntryFilter.processDefinitionKey} + + + AND zz_flow_entry.category_id = #{flowEntryFilter.categoryId} + + + AND zz_flow_entry.status = #{flowEntryFilter.status} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml new file mode 100644 index 00000000..a8c679aa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml new file mode 100644 index 00000000..68bd83ff --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryPublishVariableMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + INSERT INTO zz_flow_entry_publish_variable VALUES + + (#{item.variableId}, + #{item.entryPublishId}, + #{item.variableName}, + #{item.showName}, + #{item.variableType}, + #{item.bindDatasourceId}, + #{item.bindRelationId}, + #{item.bindColumnId}, + #{item.builtin}) + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml new file mode 100644 index 00000000..09a4ea8e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowEntryVariableMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_entry_variable.entry_id = #{flowEntryVariableFilter.entryId} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml new file mode 100644 index 00000000..5dc31fc7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageCandidateIdentityMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + DELETE FROM zz_flow_msg_candidate_identity a + WHERE EXISTS (SELECT * FROM zz_flow_message b + WHERE a.message_id = b.message_id AND b.process_instance_id = #{processInstanceId}) + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml new file mode 100644 index 00000000..60a8e4a0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageIdentityOperationMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + DELETE FROM zz_flow_msg_identity_operation a + WHERE EXISTS (SELECT * FROM zz_flow_message b + WHERE a.message_id = b.message_id AND b.process_instance_id = #{processInstanceId}) + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml new file mode 100644 index 00000000..2fcd87f5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMessageMapper.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND a.tenant_id IS NULL + + + AND a.tenant_id = #{tenantId} + + + AND a.app_code IS NULL + + + AND a.app_code = #{appCode} + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml new file mode 100644 index 00000000..732758a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowMultiInstanceTransMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml new file mode 100644 index 00000000..69323d82 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskCommentMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml new file mode 100644 index 00000000..2fca8da4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowTaskExtMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + INSERT INTO zz_flow_task_ext VALUES + + (#{item.processDefinitionId}, + #{item.taskId}, + #{item.operationListJson}, + #{item.variableListJson}, + #{item.assigneeListJson}, + #{item.groupType}, + #{item.deptPostListJson}, + #{item.roleIds}, + #{item.deptIds}, + #{item.candidateUsernames}, + #{item.copyListJson}, + #{item.extraDataJson}) + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml new file mode 100644 index 00000000..2d3867d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderExtMapper.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml new file mode 100644 index 00000000..24da5a15 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dao/mapper/FlowWorkOrderMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_flow_work_order.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + AND zz_flow_work_order.tenant_id IS NULL + + + AND zz_flow_work_order.tenant_id = #{flowWorkOrderFilter.tenantId} + + + AND zz_flow_work_order.app_code IS NULL + + + AND zz_flow_work_order.app_code = #{flowWorkOrderFilter.appCode} + + + AND zz_flow_work_order.work_order_code = #{flowWorkOrderFilter.workOrderCode} + + + AND zz_flow_work_order.process_definition_key = #{flowWorkOrderFilter.processDefinitionKey} + + + AND zz_flow_work_order.latest_approval_status = #{flowWorkOrderFilter.latestApprovalStatus} + + + AND zz_flow_work_order.flow_status = #{flowWorkOrderFilter.flowStatus} + + + AND zz_flow_work_order.create_time >= #{flowWorkOrderFilter.createTimeStart} + + + AND zz_flow_work_order.create_time <= #{flowWorkOrderFilter.createTimeEnd} + + + AND zz_flow_work_order.create_user_id = #{flowWorkOrderFilter.createUserId} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java new file mode 100644 index 00000000..05b4b875 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowCategoryDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.UpdateGroup; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 流程分类的Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程分类的Dto对象") +@Data +public class FlowCategoryDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long categoryId; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称") + @NotBlank(message = "数据验证失败,显示名称不能为空!") + private String name; + + /** + * 分类编码。 + */ + @Schema(description = "分类编码") + @NotBlank(message = "数据验证失败,分类编码不能为空!") + private String code; + + /** + * 实现顺序。 + */ + @Schema(description = "实现顺序") + @NotNull(message = "数据验证失败,实现顺序不能为空!") + private Integer showOrder; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java new file mode 100644 index 00000000..817ae003 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryDto.java @@ -0,0 +1,107 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.flow.model.constant.FlowBindFormType; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 流程的Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程的Dto对象") +@Data +public class FlowEntryDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键不能为空!", groups = {UpdateGroup.class}) + private Long entryId; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + @NotBlank(message = "数据验证失败,流程名称不能为空!") + private String processDefinitionName; + + /** + * 流程标识Key。 + */ + @Schema(description = "流程标识Key") + @NotBlank(message = "数据验证失败,流程标识Key不能为空!") + private String processDefinitionKey; + + /** + * 流程分类。 + */ + @Schema(description = "流程分类") + @NotNull(message = "数据验证失败,流程分类不能为空!") + private Long categoryId; + + /** + * 流程状态。 + */ + @Schema(description = "流程状态") + @ConstDictRef(constDictClass = FlowEntryStatus.class, message = "数据验证失败,工作流状态为无效值!") + private Integer status; + + /** + * 流程定义的xml。 + */ + @Schema(description = "流程定义的xml") + private String bpmnXml; + + /** + * 流程图类型。0: 普通流程图,1: 钉钉风格的流程图。 + */ + @Schema(description = "流程图类型。0: 普通流程图,1: 钉钉风格的流程图") + private Integer diagramType; + + /** + * 绑定表单类型。 + */ + @Schema(description = "绑定表单类型") + @ConstDictRef(constDictClass = FlowBindFormType.class, message = "数据验证失败,工作流绑定表单类型为无效值!") + @NotNull(message = "数据验证失败,工作流绑定表单类型不能为空!") + private Integer bindFormType; + + /** + * 在线表单的页面Id。 + */ + @Schema(description = "在线表单的页面Id") + private Long pageId; + + /** + * 在线表单的缺省路由名称。 + */ + @Schema(description = "在线表单的缺省路由名称") + private String defaultRouterName; + + /** + * 在线表单Id。 + */ + @Schema(description = "在线表单Id") + private Long defaultFormId; + + /** + * 工单表编码字段的编码规则,如果为空则不计算工单编码。 + */ + @Schema(description = "工单表编码字段的编码规则") + private String encodedRule; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @Schema(description = "流程的自定义扩展数据") + private String extensionData; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java new file mode 100644 index 00000000..75659d13 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowEntryVariableDto.java @@ -0,0 +1,81 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.flow.model.constant.FlowVariableType; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 流程变量Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程变量Dto对象") +@Data +public class FlowEntryVariableDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long variableId; + + /** + * 流程Id。 + */ + @Schema(description = "流程Id") + @NotNull(message = "数据验证失败,流程Id不能为空!") + private Long entryId; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + @NotBlank(message = "数据验证失败,变量名不能为空!") + private String variableName; + + /** + * 显示名。 + */ + @Schema(description = "显示名") + @NotBlank(message = "数据验证失败,显示名不能为空!") + private String showName; + + /** + * 流程变量类型。 + */ + @Schema(description = "流程变量类型") + @ConstDictRef(constDictClass = FlowVariableType.class, message = "数据验证失败,流程变量类型为无效值!") + @NotNull(message = "数据验证失败,流程变量类型不能为空!") + private Integer variableType; + + /** + * 绑定数据源Id。 + */ + @Schema(description = "绑定数据源Id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @Schema(description = "绑定数据源关联Id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @Schema(description = "绑定字段Id") + private Long bindColumnId; + + /** + * 是否内置。 + */ + @Schema(description = "是否内置") + @NotNull(message = "数据验证失败,是否内置不能为空!") + private Boolean builtin; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java new file mode 100644 index 00000000..0d616e97 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowMessageDto.java @@ -0,0 +1,51 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 工作流通知消息Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流通知消息Dto对象") +@Data +public class FlowMessageDto { + + /** + * 消息类型。 + */ + @Schema(description = "消息类型") + private Integer messageType; + + /** + * 工单Id。 + */ + @Schema(description = "工单Id") + private Long workOrderId; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程任务名称。 + */ + @Schema(description = "流程任务名称") + private String taskName; + + /** + * 更新时间范围过滤起始值(>=)。 + */ + @Schema(description = "updateTime 范围过滤起始值") + private String updateTimeStart; + + /** + * 更新时间范围过滤结束值(<=)。 + */ + @Schema(description = "updateTime 范围过滤结束值") + private String updateTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java new file mode 100644 index 00000000..4af04f6e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowTaskCommentDto.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 流程任务的批注。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务的批注") +@Data +public class FlowTaskCommentDto { + + /** + * 流程任务触发按钮类型,内置值可参考FlowTaskButton。 + */ + @Schema(description = "流程任务触发按钮类型") + @NotNull(message = "数据验证失败,任务的审批类型不能为空!") + private String approvalType; + + /** + * 流程任务的批注内容。 + */ + @Schema(description = "流程任务的批注内容") + @NotBlank(message = "数据验证失败,任务审批内容不能为空!") + private String taskComment; + + /** + * 委托指定人,比如加签、转办等。 + */ + @Schema(description = "委托指定人,比如加签、转办等") + private String delegateAssignee; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java new file mode 100644 index 00000000..f87c94c5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/dto/FlowWorkOrderDto.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.flow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 工作流工单Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流工单Dto对象") +@Data +public class FlowWorkOrderDto { + + /** + * 工单编码。 + */ + @Schema(description = "工单编码") + private String workOrderCode; + + /** + * 流程状态。参考FlowTaskStatus常量值对象。 + */ + @Schema(description = "流程状态") + private Integer flowStatus; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @Schema(description = "createTime 范围过滤起始值") + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @Schema(description = "createTime 范围过滤结束值") + private String createTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java new file mode 100644 index 00000000..02784712 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowEmptyUserException.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.exception; + +import org.flowable.common.engine.api.FlowableException; + +/** + * 流程空用户异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowEmptyUserException extends FlowableException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public FlowEmptyUserException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java new file mode 100644 index 00000000..313571e1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/exception/FlowOperationException.java @@ -0,0 +1,35 @@ +package com.orangeforms.common.flow.exception; + +/** + * 流程操作异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowOperationException extends RuntimeException { + + /** + * 构造函数。 + */ + public FlowOperationException() { + + } + + /** + * 构造函数。 + * + * @param throwable 引发异常对象。 + */ + public FlowOperationException(Throwable throwable) { + super(throwable); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public FlowOperationException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java new file mode 100644 index 00000000..4c1fce9f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/AutoSkipTaskListener.java @@ -0,0 +1,165 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.model.FlowTaskExt; +import com.orangeforms.common.flow.object.FlowTaskOperation; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowTaskCommentService; +import com.orangeforms.common.flow.service.FlowTaskExtService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.ExtensionAttribute; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.api.Task; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.*; + +/** + * 流程任务自动审批跳过的监听器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class AutoSkipTaskListener implements TaskListener { + + private final transient FlowTaskCommentService flowTaskCommentService = + ApplicationContextHolder.getBean(FlowTaskCommentService.class); + private final transient FlowApiService flowApiService = + ApplicationContextHolder.getBean(FlowApiService.class); + private final transient FlowTaskExtService flowTaskExtService = + ApplicationContextHolder.getBean(FlowTaskExtService.class); + + /** + * 流程的发起者等于当前任务的Assignee。 + */ + private static final String EQ_START_USER = "0"; + /** + * 上一步的提交者等于当前任务的Assignee。 + */ + private static final String EQ_PREV_SUBMIT_USER = "1"; + /** + * 当前任务的Assignee之前提交过审核。 + */ + private static final String EQ_HISTORIC_SUBMIT_USER = "2"; + + @Override + public void notify(DelegateTask t) { + UserTask userTask = flowApiService.getUserTask(t.getProcessDefinitionId(), t.getTaskDefinitionKey()); + List attributes = userTask.getAttributes().get(FlowConstant.USER_TASK_AUTO_SKIP_KEY); + Set skipTypes = new HashSet<>(StrUtil.split(attributes.get(0).getValue(), ",")); + String assignedUser = this.getAssignedUser(userTask, t.getProcessDefinitionId(), t.getExecutionId()); + if (StrUtil.isBlank(assignedUser)) { + return; + } + for (String skipType : skipTypes) { + if (this.verifyAndHandle(userTask, t, skipType, assignedUser)) { + return; + } + } + } + + private boolean verifyAndHandle(UserTask userTask, DelegateTask task, String skipType, String assignedUser) { + FlowTaskComment comment = null; + switch (skipType) { + case EQ_START_USER: + Object v = task.getVariable(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR); + if (ObjectUtil.equal(v, assignedUser)) { + comment = flowTaskCommentService.getFirstFlowTaskComment(task.getProcessInstanceId()); + } + break; + case EQ_PREV_SUBMIT_USER: + Object v2 = task.getVariable(FlowConstant.SUBMIT_USER_VAR); + if (ObjectUtil.equal(v2, assignedUser)) { + TokenData tokenData = TokenData.takeFromRequest(); + comment = new FlowTaskComment(); + comment.setCreateUserId(tokenData.getUserId()); + comment.setCreateLoginName(tokenData.getLoginName()); + comment.setCreateUsername(tokenData.getShowName()); + } + break; + case EQ_HISTORIC_SUBMIT_USER: + List comments = + flowTaskCommentService.getFlowTaskCommentList(task.getProcessInstanceId()); + List resultComments = new LinkedList<>(); + for (FlowTaskComment c : comments) { + if (StrUtil.equals(c.getCreateLoginName(), assignedUser)) { + resultComments.add(c); + } + } + if (CollUtil.isNotEmpty(resultComments)) { + comment = resultComments.get(0); + } + break; + default: + break; + } + if (comment != null) { + FlowTaskExt flowTaskExt = flowTaskExtService + .getByProcessDefinitionIdAndTaskId(task.getProcessDefinitionId(), userTask.getId()); + JSONObject taskVariableData = new JSONObject(); + if (StrUtil.isNotBlank(flowTaskExt.getOperationListJson())) { + List taskOperationList = + JSONArray.parseArray(flowTaskExt.getOperationListJson(), FlowTaskOperation.class); + taskOperationList.stream() + .filter(op -> op.getType().equals(FlowApprovalType.AGREE)) + .map(FlowTaskOperation::getLatestApprovalStatus).findFirst() + .ifPresent(status -> taskVariableData.put(FlowConstant.LATEST_APPROVAL_STATUS_KEY, status)); + } + Task t = flowApiService.getTaskById(task.getId()); + comment.fillWith(t); + comment.setApprovalType(FlowApprovalType.AGREE); + comment.setTaskComment(StrFormatter.format("自动跳过审批。审批人 [{}], 跳过原因 [{}]。", + userTask.getAssignee(), this.getMessageBySkipType(skipType))); + flowApiService.completeTask(t, comment, taskVariableData); + } + return comment != null; + } + + private String getAssignedUser(UserTask userTask, String processDefinitionId, String executionId) { + String assignedUser = userTask.getAssignee(); + if (StrUtil.isNotBlank(assignedUser)) { + if (assignedUser.startsWith("${") && assignedUser.endsWith("}")) { + String variableName = assignedUser.substring(2, assignedUser.length() - 1); + assignedUser = flowApiService.getExecutionVariableStringWithSafe(executionId, variableName); + } + } else { + FlowTaskExt flowTaskExt = flowTaskExtService + .getByProcessDefinitionIdAndTaskId(processDefinitionId, userTask.getId()); + List candidateUsernames; + if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { + candidateUsernames = Collections.emptyList(); + } else if (!StrUtil.equals(flowTaskExt.getCandidateUsernames(), "${" + FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR + "}")) { + candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); + } else { + String value = flowApiService + .getExecutionVariableStringWithSafe(executionId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); + candidateUsernames = value == null ? null : StrUtil.split(value, ","); + } + if (candidateUsernames != null && candidateUsernames.size() == 1) { + assignedUser = candidateUsernames.get(0); + } + } + return assignedUser; + } + + private String getMessageBySkipType(String skipType) { + return switch (skipType) { + case EQ_PREV_SUBMIT_USER -> "审批人与上一审批节点处理人相同"; + case EQ_START_USER -> "审批人为发起人"; + case EQ_HISTORIC_SUBMIT_USER -> "审批人审批过"; + default -> ""; + }; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java new file mode 100644 index 00000000..7f47ecca --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/DeptPostLeaderListener.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.flow.listener; + +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.Map; + +/** + * 当用户任务的候选组为本部门领导岗位时,该监听器会在任务创建时,获取当前流程实例发起人的部门领导。 + * 并将其指派为当前任务的候选组。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class DeptPostLeaderListener implements TaskListener { + + @Override + public void notify(DelegateTask delegateTask) { + Map variables = delegateTask.getVariables(); + if (variables.get(FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR) == null) { + delegateTask.setAssignee(variables.get(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR).toString()); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java new file mode 100644 index 00000000..417a4417 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowFinishedListener.java @@ -0,0 +1,56 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.object.GlobalThreadLocal; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; + +/** + * 流程实例监听器,在流程实例结束的时候,需要完成一些自定义的业务行为。如: + * 1. 更新流程工单表的审批状态字段。 + * 2. 业务数据同步。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class FlowFinishedListener implements ExecutionListener { + + private final transient FlowWorkOrderService flowWorkOrderService = + ApplicationContextHolder.getBean(FlowWorkOrderService.class); + private final transient FlowCustomExtFactory flowCustomExtFactory = + ApplicationContextHolder.getBean(FlowCustomExtFactory.class); + + @Override + public void notify(DelegateExecution execution) { + if (!StrUtil.equals("end", execution.getEventName())) { + return; + } + boolean enabled = GlobalThreadLocal.setDataFilter(false); + try { + String processInstanceId = execution.getProcessInstanceId(); + FlowWorkOrder workOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); + if (workOrder == null) { + return; + } + int flowStatus = FlowTaskStatus.FINISHED; + if (workOrder.getFlowStatus().equals(FlowTaskStatus.CANCELLED) + || workOrder.getFlowStatus().equals(FlowTaskStatus.STOPPED)) { + flowStatus = workOrder.getFlowStatus(); + } + workOrder.setFlowStatus(flowStatus); + // 更新流程工单中的流程状态。 + flowWorkOrderService.updateFlowStatusByProcessInstanceId(processInstanceId, flowStatus); + // 处理在线表单工作流的自定义状态更新。 + flowCustomExtFactory.getOnlineBusinessDataExtHelper().updateFlowStatus(workOrder); + } finally { + GlobalThreadLocal.setDataFilter(enabled); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java new file mode 100644 index 00000000..ba8e09ad --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowTaskNotifyListener.java @@ -0,0 +1,80 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.model.FlowTaskExt; +import com.orangeforms.common.flow.object.FlowUserTaskExtData; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowTaskExtService; +import com.orangeforms.common.flow.util.BaseFlowNotifyExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.List; + +/** + * 任务进入待办状态时的通知监听器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class FlowTaskNotifyListener implements TaskListener { + + private final transient FlowTaskExtService flowTaskExtService = + ApplicationContextHolder.getBean(FlowTaskExtService.class); + private final transient FlowApiService flowApiService = + ApplicationContextHolder.getBean(FlowApiService.class); + private final transient FlowCustomExtFactory flowCustomExtFactory = + ApplicationContextHolder.getBean(FlowCustomExtFactory.class); + + @Override + public void notify(DelegateTask delegateTask) { + String definitionId = delegateTask.getProcessDefinitionId(); + String instanceId = delegateTask.getProcessInstanceId(); + String taskId = delegateTask.getId(); + String taskKey = delegateTask.getTaskDefinitionKey(); + FlowTaskExt taskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId(definitionId, taskKey); + if (StrUtil.isBlank(taskExt.getExtraDataJson())) { + return; + } + FlowUserTaskExtData extData = JSON.parseObject(taskExt.getExtraDataJson(), FlowUserTaskExtData.class); + if (CollUtil.isEmpty(extData.getFlowNotifyTypeList())) { + return; + } + ProcessInstance instance = flowApiService.getProcessInstance(instanceId); + Object initiator = flowApiService.getProcessInstanceVariable(instanceId, FlowConstant.PROC_INSTANCE_INITIATOR_VAR); + boolean isMultiInstanceTask = flowApiService.isMultiInstanceTask(definitionId, taskKey); + Task task = flowApiService.getProcessInstanceActiveTask(instanceId, taskId); + List userInfoList = + flowTaskExtService.getCandidateUserInfoList(instanceId, taskExt, task, isMultiInstanceTask, false); + if (CollUtil.isEmpty(userInfoList)) { + log.warn("ProcessDefinition [{}] Task [{}] don't find the candidate users for notification.", + instance.getProcessDefinitionName(), task.getName()); + return; + } + BaseFlowNotifyExtHelper helper = flowCustomExtFactory.getFlowNotifyExtHelper(); + Assert.notNull(helper); + for (String notifyType : extData.getFlowNotifyTypeList()) { + FlowTaskVo flowTaskVo = new FlowTaskVo(); + flowTaskVo.setProcessDefinitionId(definitionId); + flowTaskVo.setProcessInstanceId(instanceId); + flowTaskVo.setTaskKey(taskKey); + flowTaskVo.setTaskName(delegateTask.getName()); + flowTaskVo.setTaskId(delegateTask.getId()); + flowTaskVo.setBusinessKey(instance.getBusinessKey()); + flowTaskVo.setProcessInstanceInitiator(initiator.toString()); + helper.doNotify(notifyType, userInfoList, flowTaskVo); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java new file mode 100644 index 00000000..6760fcc4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/FlowUserTaskListener.java @@ -0,0 +1,32 @@ +package com.orangeforms.common.flow.listener; + +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.Map; + +/** + * 流程任务通用监听器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class FlowUserTaskListener implements TaskListener { + + private final transient RuntimeService runtimeService = + ApplicationContextHolder.getBean(RuntimeService.class); + + @Override + public void notify(DelegateTask delegateTask) { + Map variables = delegateTask.getVariables(); + if (variables.get(FlowConstant.DELEGATE_ASSIGNEE_VAR) != null) { + delegateTask.setAssignee(variables.get(FlowConstant.DELEGATE_ASSIGNEE_VAR).toString()); + runtimeService.removeVariableLocal(delegateTask.getExecutionId(), FlowConstant.DELEGATE_ASSIGNEE_VAR); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java new file mode 100644 index 00000000..f29d6cbb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpDeptPostLeaderListener.java @@ -0,0 +1,27 @@ +package com.orangeforms.common.flow.listener; + +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +import java.util.Map; + +/** + * 当用户任务的候选组为上级部门领导岗位时,该监听器会在任务创建时,获取当前流程实例发起人的部门领导。 + * 并将其指派为当前任务的候选组。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class UpDeptPostLeaderListener implements TaskListener { + + @Override + public void notify(DelegateTask delegateTask) { + Map variables = delegateTask.getVariables(); + if (variables.get(FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR) == null) { + delegateTask.setAssignee(variables.get(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR).toString()); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java new file mode 100644 index 00000000..4b7144da --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/listener/UpdateLatestApprovalStatusListener.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.listener; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.util.ApplicationContextHolder; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.impl.el.FixedValue; + +/** + * 更新流程的最后审批状态的监听器,目前用于排他网关到任务结束节点的连线上, + * 以便于准确的判断流程实例的最后审批状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class UpdateLatestApprovalStatusListener implements ExecutionListener { + + private FixedValue latestApprovalStatus; + + private final transient FlowWorkOrderService flowWorkOrderService = + ApplicationContextHolder.getBean(FlowWorkOrderService.class); + + public void setAutoStoreVariablesExp(FixedValue approvalStatus) { + this.latestApprovalStatus = approvalStatus; + } + + @Override + public void notify(DelegateExecution execution) { + if (StrUtil.isNotBlank(latestApprovalStatus.getExpressionText())) { + FlowWorkOrder workOrder = + flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(execution.getProcessInstanceId()); + if (workOrder == null) { + return; + } + Integer approvalStatus = Integer.valueOf(latestApprovalStatus.getExpressionText()); + String processInstanceId = execution.getProcessInstanceId(); + flowWorkOrderService.updateLatestApprovalStatusByProcessInstanceId(processInstanceId, approvalStatus); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java new file mode 100644 index 00000000..9529dab1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowCategory.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程分类的实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_category") +public class FlowCategory { + + /** + * 主键Id。 + */ + @TableId(value = "category_id") + private Long categoryId; + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 显示名称。 + */ + @TableField(value = "name") + private String name; + + /** + * 分类编码。 + */ + @TableField(value = "code") + private String code; + + /** + * 实现顺序。 + */ + @TableField(value = "show_order") + private Integer showOrder; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java new file mode 100644 index 00000000..6510c1c6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntry.java @@ -0,0 +1,154 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationOneToOne; +import lombok.Data; + +import java.util.Date; + +/** + * 流程的实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_entry") +public class FlowEntry { + + /** + * 主键。 + */ + @TableId(value = "entry_id") + private Long entryId; + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 流程名称。 + */ + @TableField(value = "process_definition_name") + private String processDefinitionName; + + /** + * 流程标识Key。 + */ + @TableField(value = "process_definition_key") + private String processDefinitionKey; + + /** + * 流程分类。 + */ + @TableField(value = "category_id") + private Long categoryId; + + /** + * 工作流部署的发布主版本Id。 + */ + @TableField(value = "main_entry_publish_id") + private Long mainEntryPublishId; + + /** + * 最新发布时间。 + */ + @TableField(value = "latest_publish_time") + private Date latestPublishTime; + + /** + * 流程状态。 + */ + @TableField(value = "status") + private Integer status; + + /** + * 流程定义的xml。 + */ + @TableField(value = "bpmn_xml") + private String bpmnXml; + + /** + * 流程图类型。0: 普通流程图,1: 钉钉风格的流程图。 + */ + @TableField(value = "diagram_type") + private Integer diagramType; + + /** + * 绑定表单类型。 + */ + @TableField(value = "bind_form_type") + private Integer bindFormType; + + /** + * 在线表单的页面Id。 + */ + @TableField(value = "page_id") + private Long pageId; + + /** + * 在线表单Id。 + */ + @TableField(value = "default_form_id") + private Long defaultFormId; + + /** + * 静态表单的缺省路由名称。 + */ + @TableField(value = "default_router_name") + private String defaultRouterName; + + /** + * 工单表编码字段的编码规则,如果为空则不计算工单编码。 + */ + @TableField(value = "encoded_rule") + private String encodedRule; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @TableField(value = "extension_data") + private String extensionData; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + @TableField(exist = false) + private FlowEntryPublish mainFlowEntryPublish; + + @RelationOneToOne( + masterIdField = "categoryId", + slaveModelClass = FlowCategory.class, + slaveIdField = "categoryId") + @TableField(exist = false) + private FlowCategory flowCategory; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java new file mode 100644 index 00000000..def7bfdc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublish.java @@ -0,0 +1,89 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程发布数据的实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_entry_publish") +public class FlowEntryPublish { + + /** + * 主键Id。 + */ + @TableId(value = "entry_publish_id") + private Long entryPublishId; + + /** + * 流程Id。 + */ + @TableField(value = "entry_id") + private Long entryId; + + /** + * 流程引擎的部署Id。 + */ + @TableField(value = "deploy_id") + private String deployId; + + /** + * 流程引擎中的流程定义Id。 + */ + @TableField(value = "process_definition_id") + private String processDefinitionId; + + /** + * 发布版本。 + */ + @TableField(value = "publish_version") + private Integer publishVersion; + + /** + * 激活状态。 + */ + @TableField(value = "active_status") + private Boolean activeStatus; + + /** + * 是否为主版本。 + */ + @TableField(value = "main_version") + private Boolean mainVersion; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 发布时间。 + */ + @TableField(value = "publish_time") + private Date publishTime; + + /** + * 第一个非开始节点任务的附加信息。 + */ + @TableField(value = "init_task_info") + private String initTaskInfo; + + /** + * 分析后的节点JSON信息。 + */ + @TableField(value = "analyzed_node_json") + private String analyzedNodeJson; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @TableField(value = "extension_data") + private String extensionData; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java new file mode 100644 index 00000000..be7965ec --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryPublishVariable.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * FlowEntryPublishVariable实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_entry_publish_variable") +public class FlowEntryPublishVariable { + + /** + * 主键Id。 + */ + @TableId(value = "variable_id") + private Long variableId; + + /** + * 流程Id。 + */ + @TableField(value = "entry_publish_id") + private Long entryPublishId; + + /** + * 变量名。 + */ + @TableField(value = "variable_name") + private String variableName; + + /** + * 显示名。 + */ + @TableField(value = "show_name") + private String showName; + + /** + * 变量类型。 + */ + @TableField(value = "variable_type") + private Integer variableType; + + /** + * 是否内置。 + */ + @TableField(value = "builtin") + private Boolean builtin; + + /** + * 绑定数据源Id。 + */ + @TableField(value = "bind_datasource_id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @TableField(value = "bind_relation_id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @TableField(value = "bind_column_id") + private Long bindColumnId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java new file mode 100644 index 00000000..bbb8df66 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowEntryVariable.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程变量实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_entry_variable") +public class FlowEntryVariable { + + /** + * 主键Id。 + */ + @TableId(value = "variable_id") + private Long variableId; + + /** + * 流程Id。 + */ + @TableField(value = "entry_id") + private Long entryId; + + /** + * 变量名。 + */ + @TableField(value = "variable_name") + private String variableName; + + /** + * 显示名。 + */ + @TableField(value = "show_name") + private String showName; + + /** + * 流程变量类型。 + */ + @TableField(value = "variable_type") + private Integer variableType; + + /** + * 绑定数据源Id。 + */ + @TableField(value = "bind_datasource_id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @TableField(value = "bind_relation_id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @TableField(value = "bind_column_id") + private Long bindColumnId; + + /** + * 是否内置。 + */ + @TableField(value = "builtin") + private Boolean builtin; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java new file mode 100644 index 00000000..e466ec36 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessage.java @@ -0,0 +1,167 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 工作流通知消息实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_message") +public class FlowMessage { + + /** + * 主键Id。 + */ + @TableId(value = "message_id") + private Long messageId; + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 消息类型。 + */ + @TableField(value = "message_type") + private Integer messageType; + + /** + * 消息内容。 + */ + @TableField(value = "message_content") + private String messageContent; + + /** + * 催办次数。 + */ + @TableField(value = "remind_count") + private Integer remindCount; + + /** + * 工单Id。 + */ + @TableField(value = "work_order_id") + private Long workOrderId; + + /** + * 流程定义Id。 + */ + @TableField(value = "process_definition_id") + private String processDefinitionId; + + /** + * 流程定义标识。 + */ + @TableField(value = "process_definition_key") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @TableField(value = "process_definition_name") + private String processDefinitionName; + + /** + * 流程实例Id。 + */ + @TableField(value = "process_instance_id") + private String processInstanceId; + + /** + * 流程实例发起者。 + */ + @TableField(value = "process_instance_initiator") + private String processInstanceInitiator; + + /** + * 流程任务Id。 + */ + @TableField(value = "task_id") + private String taskId; + + /** + * 流程任务定义标识。 + */ + @TableField(value = "task_definition_key") + private String taskDefinitionKey; + + /** + * 流程任务名称。 + */ + @TableField(value = "task_name") + private String taskName; + + /** + * 创建时间。 + */ + @TableField(value = "task_start_time") + private Date taskStartTime; + + /** + * 任务指派人登录名。 + */ + @TableField(value = "task_assignee") + private String taskAssignee; + + /** + * 任务是否已完成。 + */ + @TableField(value = "task_finished") + private Boolean taskFinished; + + /** + * 业务数据快照。 + */ + @TableField(value = "business_data_shot") + private String businessDataShot; + + /** + * 是否为在线表单消息数据。 + */ + @TableField(value = "online_form_data") + private Boolean onlineFormData; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 创建者显示名。 + */ + @TableField(value = "create_username") + private String createUsername; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java new file mode 100644 index 00000000..75cdb858 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageCandidateIdentity.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 流程任务消息的候选身份实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_msg_candidate_identity") +public class FlowMessageCandidateIdentity { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 任务消息Id。 + */ + @TableField(value = "message_id") + private Long messageId; + + /** + * 候选身份类型。 + */ + @TableField(value = "candidate_type") + private String candidateType; + + /** + * 候选身份Id。 + */ + @TableField(value = "candidate_id") + private String candidateId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java new file mode 100644 index 00000000..9dc9a78c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMessageIdentityOperation.java @@ -0,0 +1,48 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 流程任务消息所属用户的操作表。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_msg_identity_operation") +public class FlowMessageIdentityOperation { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 任务消息Id。 + */ + @TableField(value = "message_id") + private Long messageId; + + /** + * 用户登录名。 + */ + @TableField(value = "login_name") + private String loginName; + + /** + * 操作类型。 + * 常量值参考FlowMessageOperationType对象。 + */ + @TableField(value = "operation_type") + private Integer operationType; + + /** + * 操作时间。 + */ + @TableField(value = "operation_time") + private Date operationTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java new file mode 100644 index 00000000..fe2f18a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowMultiInstanceTrans.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.flowable.task.api.TaskInfo; + +import java.util.Date; + +/** + * 流程多实例任务执行流水对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +@TableName(value = "zz_flow_multi_instance_trans") +public class FlowMultiInstanceTrans { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 流程实例Id。 + */ + @TableField(value = "process_instance_id") + private String processInstanceId; + + /** + * 任务Id。 + */ + @TableField(value = "task_id") + private String taskId; + + /** + * 任务标识。 + */ + @TableField(value = "task_key") + private String taskKey; + + /** + * 会签任务的执行Id。 + */ + @TableField(value = "multi_instance_exec_id") + private String multiInstanceExecId; + + /** + * 任务的执行Id。 + */ + @TableField(value = "execution_id") + private String executionId; + + /** + * 会签指派人列表。 + */ + @TableField(value = "assignee_list") + private String assigneeList; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 创建者登录名。 + */ + @TableField(value = "create_login_name") + private String createLoginName; + + /** + * 创建者显示名。 + */ + @TableField(value = "create_username") + private String createUsername; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + public FlowMultiInstanceTrans(TaskInfo task) { + this.fillWith(task); + } + + public void fillWith(TaskInfo task) { + this.taskId = task.getId(); + this.taskKey = task.getTaskDefinitionKey(); + this.processInstanceId = task.getProcessInstanceId(); + this.executionId = task.getExecutionId(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java new file mode 100644 index 00000000..d6959dfc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskComment.java @@ -0,0 +1,150 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.util.ContextUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.flowable.task.api.TaskInfo; + +import java.util.Date; + +/** + * FlowTaskComment实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@NoArgsConstructor +@TableName(value = "zz_flow_task_comment") +public class FlowTaskComment { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 流程实例Id。 + */ + @TableField(value = "process_instance_id") + private String processInstanceId; + + /** + * 任务Id。 + */ + @TableField(value = "task_id") + private String taskId; + + /** + * 任务标识。 + */ + @TableField(value = "task_key") + private String taskKey; + + /** + * 任务名称。 + */ + @TableField(value = "task_name") + private String taskName; + + /** + * 用于驳回和自由跳的目标任务标识。 + */ + @TableField(value = "target_task_key") + private String targetTaskKey; + + /** + * 任务的执行Id。 + */ + @TableField(value = "execution_id") + private String executionId; + + /** + * 会签任务的执行Id。 + */ + @TableField(value = "multi_instance_exec_id") + private String multiInstanceExecId; + + /** + * 审批类型。 + */ + @TableField(value = "approval_type") + private String approvalType; + + /** + * 批注内容。 + */ + @TableField(value = "task_comment") + private String taskComment; + + /** + * 委托指定人,比如加签、转办等。 + */ + @TableField(value = "delegate_assignee") + private String delegateAssignee; + + /** + * 自定义数据。开发者可自行扩展,推荐使用JSON格式数据。 + */ + @TableField(value = "custom_business_data") + private String customBusinessData; + + /** + * 审批人头像。 + */ + @TableField(value = "head_image_url") + private String headImageUrl; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 创建者登录名。 + */ + @TableField(value = "create_login_name") + private String createLoginName; + + /** + * 创建者显示名。 + */ + @TableField(value = "create_username") + private String createUsername; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + private static final String REQ_ATTRIBUTE_KEY = "flowTaskComment"; + + public FlowTaskComment(TaskInfo task) { + this.fillWith(task); + } + + public static void setToRequest(FlowTaskComment comment) { + if (ContextUtil.getHttpRequest() != null) { + ContextUtil.getHttpRequest().setAttribute(REQ_ATTRIBUTE_KEY, comment); + } + } + + public static FlowTaskComment getFromRequest() { + if (ContextUtil.getHttpRequest() == null) { + return null; + } + return (FlowTaskComment) ContextUtil.getHttpRequest().getAttribute(REQ_ATTRIBUTE_KEY); + } + + public void fillWith(TaskInfo task) { + this.taskId = task.getId(); + this.taskKey = task.getTaskDefinitionKey(); + this.taskName = task.getName(); + this.processInstanceId = task.getProcessInstanceId(); + this.executionId = task.getExecutionId(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java new file mode 100644 index 00000000..725c9ac0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowTaskExt.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 流程任务扩展实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_task_ext") +public class FlowTaskExt { + + /** + * 流程引擎的定义Id。 + */ + @TableField(value = "process_definition_id") + private String processDefinitionId; + + /** + * 流程引擎任务Id。 + */ + @TableField(value = "task_id") + private String taskId; + + /** + * 操作列表JSON。 + */ + @TableField(value = "operation_list_json") + private String operationListJson; + + /** + * 变量列表JSON。 + */ + @TableField(value = "variable_list_json") + private String variableListJson; + + /** + * 存储多实例的assigneeList的JSON。 + */ + @TableField(value = "assignee_list_json") + private String assigneeListJson; + + /** + * 分组类型。 + */ + @TableField(value = "group_type") + private String groupType; + + /** + * 保存岗位相关的数据。 + */ + @TableField(value = "dept_post_list_json") + private String deptPostListJson; + + /** + * 逗号分隔的角色Id。 + */ + @TableField(value = "role_ids") + private String roleIds; + + /** + * 逗号分隔的部门Id。 + */ + @TableField(value = "dept_ids") + private String deptIds; + + /** + * 逗号分隔候选用户名。 + */ + @TableField(value = "candidate_usernames") + private String candidateUsernames; + + /** + * 抄送相关的数据。 + */ + @TableField(value = "copy_list_json") + private String copyListJson; + + /** + * 用户任务的扩展属性,存储为JSON的字符串格式。 + */ + @TableField(value = "extra_data_json") + private String extraDataJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java new file mode 100644 index 00000000..1ac6fcfe --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrder.java @@ -0,0 +1,163 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.DeptFilterColumn; +import com.orangeforms.common.core.annotation.UserFilterColumn; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 工作流工单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_work_order") +public class FlowWorkOrder { + + /** + * 主键Id。 + */ + @TableId(value = "work_order_id") + private Long workOrderId; + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 工单编码字段。 + */ + @TableField(value = "work_order_code") + private String workOrderCode; + + /** + * 流程定义标识。 + */ + @TableField(value = "process_definition_key") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @TableField(value = "process_definition_name") + private String processDefinitionName; + + /** + * 流程引擎的定义Id。 + */ + @TableField(value = "process_definition_id") + private String processDefinitionId; + + /** + * 流程实例Id。 + */ + @TableField(value = "process_instance_id") + private String processInstanceId; + + /** + * 在线表单的主表Id。 + */ + @TableField(value = "online_table_id") + private Long onlineTableId; + + /** + * 静态表单所使用的数据表名。 + */ + @TableField(value = "table_name") + private String tableName; + + /** + * 业务主键值。 + */ + @TableField(value = "business_key") + private String businessKey; + + /** + * 最近的审批状态。 + */ + @TableField(value = "latest_approval_status") + private Integer latestApprovalStatus; + + /** + * 流程状态。参考FlowTaskStatus常量值对象。 + */ + @TableField(value = "flow_status") + private Integer flowStatus; + + /** + * 提交用户登录名称。 + */ + @TableField(value = "submit_username") + private String submitUsername; + + /** + * 提交用户所在部门Id。 + */ + @DeptFilterColumn + @TableField(value = "dept_id") + private Long deptId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @UserFilterColumn + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @TableField(exist = false) + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @TableField(exist = false) + private String createTimeEnd; + + @RelationConstDict( + masterIdField = "flowStatus", + constantDictClass = FlowTaskStatus.class) + @TableField(exist = false) + private Map flowStatusDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java new file mode 100644 index 00000000..ef0f515a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/FlowWorkOrderExt.java @@ -0,0 +1,72 @@ +package com.orangeforms.common.flow.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.util.Date; + +/** + * 工作流工单扩展数据实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_flow_work_order_ext") +public class FlowWorkOrderExt { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 流程工单Id。 + */ + @TableField(value = "work_order_id") + private Long workOrderId; + + /** + * 草稿数据。 + */ + @TableField(value = "draft_data") + private String draftData; + + /** + * 业务数据。 + */ + @TableField(value = "business_data") + private String businessData; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java new file mode 100644 index 00000000..37de6e36 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowBindFormType.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 工作流绑定表单类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowBindFormType { + + /** + * 在线表单。 + */ + public static final int ONLINE_FORM = 0; + /** + * 路由表单。 + */ + public static final int ROUTER_FORM = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(ONLINE_FORM, "在线表单"); + DICT_MAP.put(ROUTER_FORM, "路由表单"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowBindFormType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java new file mode 100644 index 00000000..826e9895 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowEntryStatus.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 工作流状态。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowEntryStatus { + + /** + * 未发布。 + */ + public static final int UNPUBLISHED = 0; + /** + * 已发布。 + */ + public static final int PUBLISHED = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(UNPUBLISHED, "未发布"); + DICT_MAP.put(PUBLISHED, "已发布"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowEntryStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java new file mode 100644 index 00000000..6bd62cfd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageOperationType.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.flow.model.constant; + +/** + * 工作流消息操作类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowMessageOperationType { + + /** + * 已读操作。 + */ + public static final int READ_FINISHED = 0; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowMessageOperationType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java new file mode 100644 index 00000000..18d41da2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowMessageType.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.flow.model.constant; + +/** + * 工作流消息类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowMessageType { + + /** + * 催办消息。 + */ + public static final int REMIND_TYPE = 0; + + /** + * 抄送消息。 + */ + public static final int COPY_TYPE = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowMessageType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java new file mode 100644 index 00000000..f68f49ad --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/model/constant/FlowVariableType.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.flow.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 流程变量类型。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FlowVariableType { + + /** + * 流程实例变量。 + */ + public static final int INSTANCE = 0; + /** + * 任务变量。 + */ + public static final int TASK = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(INSTANCE, "流程实例变量"); + DICT_MAP.put(TASK, "任务变量"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowVariableType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java new file mode 100644 index 00000000..a3277c29 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowElementExtProperty.java @@ -0,0 +1,18 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +/** + * 流程任务的扩展属性。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowElementExtProperty { + + /** + * 最近的审批状态,该值目前仅仅用于流程线元素,即SequenceElement。 + */ + private Integer latestApprovalStatus; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java new file mode 100644 index 00000000..fec564d5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowEntryExtensionData.java @@ -0,0 +1,37 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 流程扩展数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowEntryExtensionData { + + /** + * 通知类型。 + */ + private List notifyTypes; + /** + * 流程审批状态字典数据列表。Map的key是id和name。 + */ + private List> approvalStatusDict; + /** + * 级联删除业务数据。 + */ + private Boolean cascadeDeleteBusinessData = false; + /** + * 是否支持流程复活。 + */ + private Boolean supportRevive = false; + /** + * 复活数据保留天数。0表示永久保留。 + */ + private Integer keptReviveDays = 0; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java new file mode 100644 index 00000000..978284c7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowRumtimeObject.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; + +/** + * 工作流运行时常用对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowRumtimeObject { + + /** + * 运行时流程实例对象。 + */ + private ProcessInstance instance; + /** + * 运行时流程任务对象。 + */ + private Task task; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java new file mode 100644 index 00000000..55c1388b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskMultiSignAssign.java @@ -0,0 +1,22 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +/** + * 表示多实例任务的指派人信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowTaskMultiSignAssign { + + /** + * 指派人类型。参考常量类 UserFilterGroup。 + */ + private String assigneeType; + /** + * 逗号分隔的指派人列表。 + */ + private String assigneeList; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java new file mode 100644 index 00000000..8ad86d88 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskOperation.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +/** + * 流程图中的用户任务操作数据。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowTaskOperation { + + /** + * 操作Id。 + */ + private String id; + /** + * 操作的标签名。 + */ + private String label; + /** + * 操作类型。 + */ + private String type; + /** + * 显示顺序。 + */ + private Integer showOrder; + /** + * 最后审批状态。 + */ + private Integer latestApprovalStatus; + /** + * 在流程图中定义的多实例会签的指定人员信息。 + */ + private FlowTaskMultiSignAssign multiSignAssignee; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java new file mode 100644 index 00000000..5e954d8f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowTaskPostCandidateGroup.java @@ -0,0 +1,64 @@ +package com.orangeforms.common.flow.object; + +import com.orangeforms.common.flow.constant.FlowConstant; +import lombok.Data; + +import java.util.LinkedList; +import java.util.List; + +/** + * 流程任务岗位候选组数据。仅用于流程任务的候选组类型为岗位时。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowTaskPostCandidateGroup { + + /** + * 唯一值,目前仅前端使用。 + */ + private String id; + /** + * 岗位类型。 + * 1. 所有部门岗位审批变量,值为 (allDeptPost)。 + * 2. 本部门岗位审批变量,值为 (selfDeptPost)。 + * 3. 上级部门岗位审批变量,值为 (upDeptPost)。 + * 4. 任意部门关联的岗位审批变量,值为 (deptPost)。 + */ + private String type; + /** + * 岗位Id。type为(1,2,3)时使用该值。 + */ + private String postId; + /** + * 部门岗位Id。type为(4)时使用该值。 + */ + private String deptPostId; + + public static List buildCandidateGroupList(List groupDataList) { + List candidateGroupList = new LinkedList<>(); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR: + candidateGroupList.add(groupData.getPostId()); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_VAR: + candidateGroupList.add(groupData.getDeptPostId()); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + candidateGroupList.add("${" + FlowConstant.SELF_DEPT_POST_PREFIX + groupData.getPostId() + "}"); + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + candidateGroupList.add("${" + FlowConstant.SIBLING_DEPT_POST_PREFIX + groupData.getPostId() + "}"); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + candidateGroupList.add("${" + FlowConstant.UP_DEPT_POST_PREFIX + groupData.getPostId() + "}"); + break; + default: + break; + } + } + return candidateGroupList; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java new file mode 100644 index 00000000..85d8a7a3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/object/FlowUserTaskExtData.java @@ -0,0 +1,63 @@ +package com.orangeforms.common.flow.object; + +import lombok.Data; + +import java.util.List; + +/** + * 流程用户任务扩展数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class FlowUserTaskExtData { + + public static final String NOTIFY_TYPE_MSG = "message"; + public static final String NOTIFY_TYPE_EMAIL = "email"; + + public static final String TIMEOUT_AUTO_COMPLETE = "autoComplete"; + public static final String TIMEOUT_SEND_MSG = "sendMessage"; + + public static final String EMPTY_USER_TO_ASSIGNEE = "toAssignee"; + public static final String EMPTY_USER_AUTO_REJECT = "autoReject"; + public static final String EMPTY_USER_AUTO_COMPLETE = "autoComplete"; + + /** + * 拒绝后再提交,走重新审批。 + */ + public static final String REJECT_TYPE_REDO = "0"; + /** + * 拒绝后再提交,直接回到驳回前的节点。 + */ + public static final String REJECT_TYPE_BACK_TO_SOURCE = "1"; + + /** + * 任务通知类型列表。 + */ + private List flowNotifyTypeList; + /** + * 拒绝后再次提交的审批类型。 + */ + private String rejectType = REJECT_TYPE_REDO; + /** + * 到期提醒的小时数(从待办任务被创建的时候开始计算)。 + */ + private Integer timeoutHours; + /** + * 任务超时的处理方式。 + */ + private String timeoutHandleWay; + /** + * 默认审批人。 + */ + private String defaultAssignee; + /** + * 空用户审批处理方式。 + */ + private String emptyUserHandleWay; + /** + * 空用户审批时设定的审批人。 + */ + private String emptyUserToAssignee; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java new file mode 100644 index 00000000..892ea587 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowApiService.java @@ -0,0 +1,568 @@ +package com.orangeforms.common.flow.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.flow.model.FlowTaskComment; +import com.orangeforms.common.flow.model.FlowTaskExt; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FieldExtension; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; + +import javax.xml.stream.XMLStreamException; +import java.text.ParseException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 流程引擎API的接口封装服务。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowApiService { + + /** + * 启动流程实例。 + * + * @param processDefinitionId 流程定义Id。 + * @param dataId 业务主键Id。 + * @return 新启动的流程实例。 + */ + ProcessInstance start(String processDefinitionId, Object dataId); + + /** + * 完成第一个用户任务。 + * + * @param processInstanceId 流程实例Id。 + * @param flowTaskComment 审批对象。 + * @param taskVariableData 流程任务的变量数据。 + * @return 新完成的任务对象。 + */ + Task takeFirstTask(String processInstanceId, FlowTaskComment flowTaskComment, JSONObject taskVariableData); + + /** + * 启动流程实例,如果当前登录用户为第一个用户任务的指派者,或者Assginee为流程启动人变量时, + * 则自动完成第一个用户任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param dataId 当前流程主表的主键数据。 + * @param flowTaskComment 审批对象。 + * @param taskVariableData 流程任务的变量数据。 + * @return 新启动的流程实例。 + */ + ProcessInstance startAndTakeFirst( + String processDefinitionId, Object dataId, FlowTaskComment flowTaskComment, JSONObject taskVariableData); + + /** + * 多实例加签减签。 + * + * @param startTaskInstance 会签对象的发起任务实例。 + * @param multiInstanceActiveTask 正在执行的多实例任务对象。 + * @param newAssignees 新指派人,多个指派人之间逗号分隔。 + * @param isAdd 是否为加签。 + */ + void submitConsign(HistoricTaskInstance startTaskInstance, Task multiInstanceActiveTask, String newAssignees, boolean isAdd); + + /** + * 完成任务,同时提交审批数据。 + * + * @param task 工作流任务对象。 + * @param flowTaskComment 审批对象。 + * @param taskVariableData 流程任务的变量数据。 + */ + void completeTask(Task task, FlowTaskComment flowTaskComment, JSONObject taskVariableData); + + /** + * 判断当前登录用户是否为流程实例中的用户任务的指派人。或是候选人之一,如果是候选人则拾取该任务并成为指派人。 + * 如果都不是,就会返回具体的错误信息。 + * + * @param task 流程实例中的用户任务。 + * @return 调用结果。 + */ + CallResult verifyAssigneeOrCandidateAndClaim(Task task); + + /** + * 初始化并返回流程实例的变量Map。 + * + * @param processDefinitionId 流程定义Id。 + * @return 初始化后的流程实例变量Map。 + */ + Map initAndGetProcessInstanceVariables(String processDefinitionId); + + /** + * 判断当前登录用户是否为流程实例中的用户任务的指派人。或是候选人之一。 + * + * @param task 流程实例中的用户任务。 + * @return 是返回true,否则false。 + */ + boolean isAssigneeOrCandidate(TaskInfo task); + + /** + * 获取指定流程定义的全部流程节点。 + * + * @param processDefinitionId 流程定义Id。 + * @return 当前流程定义的全部节点集合。 + */ + Collection getProcessAllElements(String processDefinitionId); + + /** + * 判断当前登录用户是否为流程实例的发起人。 + * + * @param processInstanceId 流程实例Id。 + * @return 是返回true,否则false。 + */ + boolean isProcessInstanceStarter(String processInstanceId); + + /** + * 为流程实例设置BusinessKey。 + * + * @param processInstanceId 流程实例Id。 + * @param dataId 通常为主表的主键Id。 + */ + void setBusinessKeyForProcessInstance(String processInstanceId, Object dataId); + + /** + * 判断指定的流程实例Id是否存在。 + * + * @param processInstanceId 流程实例Id。 + * @return 存在返回true,否则false。 + */ + boolean existActiveProcessInstance(String processInstanceId); + + /** + * 获取指定的流程实例对象。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例对象。 + */ + ProcessInstance getProcessInstance(String processInstanceId); + + /** + * 获取指定的流程实例对象。 + * + * @param processDefinitionId 流程定义Id。 + * @param businessKey 业务主键Id。 + * @return 流程实例对象。 + */ + ProcessInstance getProcessInstanceByBusinessKey(String processDefinitionId, String businessKey); + + /** + * 获取流程实例的列表。 + * + * @param processInstanceIdSet 流程实例Id集合。 + * @return 流程实例列表。 + */ + List getProcessInstanceList(Set processInstanceIdSet); + + /** + * 根据流程定义Id查询流程定义对象。 + * + * @param processDefinitionId 流程定义Id。 + * @return 流程定义对象。 + */ + ProcessDefinition getProcessDefinitionById(String processDefinitionId); + + /** + * 根据流程部署Id查询流程定义对象。 + * + * @param deployId 流程部署Id。 + * @return 流程定义对象。 + */ + ProcessDefinition getProcessDefinitionByDeployId(String deployId); + + /** + * 获取流程定义的列表。 + * + * @param processDefinitionIdSet 流程定义Id集合。 + * @return 流程定义列表。 + */ + List getProcessDefinitionList(Set processDefinitionIdSet); + + /** + * 挂起流程定义对象。 + * + * @param processDefinitionId 流程定义Id。 + */ + void suspendProcessDefinition(String processDefinitionId); + + /** + * 激活流程定义对象。 + * + * @param processDefinitionId 流程定义Id。 + */ + void activateProcessDefinition(String processDefinitionId); + + /** + * 获取指定流程定义的BpmnModel。 + * + * @param processDefinitionId 流程定义Id。 + * @return 关联的BpmnModel。 + */ + BpmnModel getBpmnModelByDefinitionId(String processDefinitionId); + + /** + * 判断任务是否为多实例任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param taskKey 流程任务标识。 + * @return true为多实例,否则false。 + */ + boolean isMultiInstanceTask(String processDefinitionId, String taskKey); + + /** + * 设置流程实例的变量集合。 + * + * @param processInstanceId 流程实例Id。 + * @param variableMap 变量名。 + */ + void setProcessInstanceVariables(String processInstanceId, Map variableMap); + + /** + * 获取流程实例的变量。 + * + * @param processInstanceId 流程实例Id。 + * @param variableName 变量名。 + * @return 变量值。 + */ + Object getProcessInstanceVariable(String processInstanceId, String variableName); + + /** + * 获取指定流程实例和任务Id的当前活动任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @return 当前流程实例的活动任务。 + */ + Task getProcessInstanceActiveTask(String processInstanceId, String taskId); + + /** + * 获取指定流程实例的当前活动任务列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 当前流程实例的活动任务。 + */ + List getProcessInstanceActiveTaskList(String processInstanceId); + + /** + * 获取指定流程实例的当前活动任务列表,同时转换为流出任务视图对象列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 当前流程实例的活动任务。 + */ + List getProcessInstanceActiveTaskListAndConvert(String processInstanceId); + + /** + * 根据任务Id,获取当前运行时任务。 + * + * @param taskId 任务Id。 + * @return 运行时任务对象。 + */ + Task getTaskById(String taskId); + + /** + * 获取用户的任务列表。这其中包括当前用户作为指派人和候选人。 + * + * @param username 指派人。 + * @param definitionKey 流程定义的标识。 + * @param definitionName 流程定义名。 + * @param taskName 任务名称。 + * @param pageParam 分页对象。 + * @return 用户的任务列表。 + */ + MyPageData getTaskListByUserName( + String username, String definitionKey, String definitionName, String taskName, MyPageParam pageParam); + + /** + * 获取用户的任务数量。这其中包括当前用户作为指派人和候选人。 + * + * @param username 指派人。 + * @return 用户的任务数量。 + */ + long getTaskCountByUserName(String username); + + /** + * 获取流程实例Id集合的运行时任务列表。 + * + * @param processInstanceIdSet 流程实例Id集合。 + * @return 运行时任务列表。 + */ + List getTaskListByProcessInstanceIds(List processInstanceIdSet); + + /** + * 将流程任务列表数据,转换为前端可以显示的流程对象。 + * + * @param taskList 流程引擎中的任务列表。 + * @return 前端可以显示的流程任务列表。 + */ + List convertToFlowTaskList(List taskList); + + /** + * 添加流程实例结束的监听器。 + * + * @param bpmnModel 流程模型。 + * @param listenerClazz 流程监听器的Class对象。 + */ + void addProcessInstanceEndListener(BpmnModel bpmnModel, Class listenerClazz); + + /** + * 添加流程任务的执行监听器。 + * + * @param flowElement 指定任务节点。 + * @param listenerClazz 执行监听器。 + * @param event 事件。 + * @param fieldExtensions 执行监听器的扩展变量列表。 + */ + void addExecutionListener( + FlowElement flowElement, + Class listenerClazz, + String event, + List fieldExtensions); + + /** + * 添加流程任务创建的任务监听器。 + * + * @param userTask 用户任务。 + * @param listenerClazz 任务监听器。 + */ + void addTaskCreateListener(UserTask userTask, Class listenerClazz); + + /** + * 获取流程实例的历史流程实例。 + * + * @param processInstanceId 流程实例Id。 + * @return 历史流程实例。 + */ + HistoricProcessInstance getHistoricProcessInstance(String processInstanceId); + + /** + * 获取流程实例的历史流程实例列表。 + * + * @param processInstanceIdSet 流程实例Id集合。 + * @return 历史流程实例列表。 + */ + List getHistoricProcessInstanceList(Set processInstanceIdSet); + + /** + * 查询历史流程实例的列表。 + * + * @param processDefinitionKey 流程标识名。 + * @param processDefinitionName 流程名。 + * @param startUser 流程发起用户。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @param finishedOnly 仅仅返回已经结束的流程。 + * @return 分页后的查询列表对象。 + * @throws ParseException 日期参数解析失败。 + */ + MyPageData getHistoricProcessInstanceList( + String processDefinitionKey, + String processDefinitionName, + String startUser, + String beginDate, + String endDate, + MyPageParam pageParam, + boolean finishedOnly) throws ParseException; + + /** + * 获取流程实例的已完成历史任务列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例已完成的历史任务列表。 + */ + List getHistoricActivityInstanceList(String processInstanceId); + + /** + * 获取流程实例的已完成历史任务列表,同时按照每个活动实例的开始时间升序排序。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例已完成的历史任务列表。 + */ + List getHistoricActivityInstanceListOrderByStartTime(String processInstanceId); + + /** + * 获取当前用户的历史已办理任务列表。 + * + * @param processDefinitionName 流程名。 + * @param beginDate 流程发起开始时间。 + * @param endDate 流程发起结束时间。 + * @param pageParam 分页对象。 + * @return 分页后的查询列表对象。 + * @throws ParseException 日期参数解析失败。 + */ + MyPageData getHistoricTaskInstanceFinishedList( + String processDefinitionName, + String beginDate, + String endDate, + MyPageParam pageParam) throws ParseException; + + /** + * 获取指定的历史任务实例。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 任务Id。 + * @return 历史任务实例。 + */ + HistoricTaskInstance getHistoricTaskInstance(String processInstanceId, String taskId); + + /** + * 获取流程实例的待完成任务列表。 + * + * @param processInstanceId 流程实例Id。 + * @return 流程实例待完成的任务列表。 + */ + List getHistoricUnfinishedInstanceList(String processInstanceId); + + /** + * 终止流程实例,将任务从当前节点直接流转到主流程的结束事件。 + * + * @param processInstanceId 流程实例Id。 + * @param stopReason 停止原因。 + * @param forCancel 是否由取消工单触发。 + * @return 执行结果。 + */ + CallResult stopProcessInstance(String processInstanceId, String stopReason, boolean forCancel); + + /** + * 终止流程实例,将任务从当前节点直接流转到主流程的结束事件。 + * + * @param processInstanceId 流程实例Id。 + * @param stopReason 停止原因。 + * @param status 流程状态。 + * @return 执行结果。 + */ + CallResult stopProcessInstance(String processInstanceId, String stopReason, int status); + + /** + * 删除流程实例。 + * + * @param processInstanceId 流程实例Id。 + */ + void deleteProcessInstance(String processInstanceId); + + /** + * 获取任务的指定本地变量。 + * + * @param taskId 任务Id。 + * @param variableName 变量名。 + * @return 变量值。 + */ + Object getTaskVariable(String taskId, String variableName); + + /** + * 安全的获取任务变量,并返回字符型的变量值。 + * + * @param taskId 任务Id。 + * @param variableName 变量名。 + * @return 返回变量值的字符串形式,如果变量不存在不会抛异常,返回null。 + */ + String getTaskVariableStringWithSafe(String taskId, String variableName); + + /** + * 获取任务执行时的指定本地变量。 + * + * @param executionId 任务执行时Id。 + * @param variableName 变量名。 + * @return 变量值。 + */ + Object getExecutionVariable(String executionId, String variableName); + + /** + * 安全的获取任务执行时变量,并返回字符型的变量值。 + * + * @param executionId 任务执行时Id。 + * @param variableName 变量名。 + * @return 返回变量值的字符串形式,如果变量不存在不会抛异常,返回null。 + */ + String getExecutionVariableStringWithSafe(String executionId, String variableName); + + /** + * 获取历史流程变量。 + * + * @param processInstanceId 流程实例Id。 + * @param variableName 变量名。 + * @return 获取历史流程变量。 + */ + Object getHistoricProcessInstanceVariable(String processInstanceId, String variableName); + + /** + * 将xml格式的流程模型字符串,转换为标准的流程模型。 + * + * @param bpmnXml xml格式的流程模型字符串。 + * @return 转换后的标准的流程模型。 + * @throws XMLStreamException XML流处理异常 + */ + BpmnModel convertToBpmnModel(String bpmnXml) throws XMLStreamException; + + /** + * 回退到上一个用户任务节点。如果没有指定,则回退到上一个任务。 + * + * @param task 当前活动任务。 + * @param targetKey 指定回退到的任务标识。如果为null,则回退到上一个任务。 + * @param forReject true表示驳回,false为撤回。 + * @param reason 驳回或者撤销的原因。 + * @return 回退结果。 + */ + CallResult backToRuntimeTask(Task task, String targetKey, boolean forReject, String reason); + + /** + * 转办任务给他人。 + * + * @param task 流程任务。 + * @param flowTaskComment 审批对象。 + */ + void transferTo(Task task, FlowTaskComment flowTaskComment); + + /** + * 获取当前任务在流程图中配置候选用户组数据。 + * + * @param flowTaskExt 流程任务扩展对象。 + * @param taskId 运行时任务Id。 + * @return 候选用户组数据。 + */ + List getCandidateUsernames(FlowTaskExt flowTaskExt, String taskId); + + /** + * 获取当前任务在流程图中配置到的部门岗位Id集合和岗位Id集合。 + * + * @param flowTaskExt 流程任务扩展对象。 + * @param processInstanceId 流程实例Id。 + * @param historic 是否为历史任务。 + * @return first为部门岗位Id集合,second是岗位Id集合。 + */ + Tuple2, Set> getDeptPostIdAndPostIds( + FlowTaskExt flowTaskExt, String processInstanceId, boolean historic); + + /** + * 获取流程图中所有用户任务的映射。 + * + * @param processDefinitionId 流程定义Id。 + * @return 流程图中所有用户任务的映射。 + */ + Map getAllUserTaskMap(String processDefinitionId); + + /** + * 获取流程图中指定的用户任务。 + * + * @param processDefinitionId 流程定义Id。 + * @param taskKey 用户任务标识。 + * @return 用户任务。 + */ + UserTask getUserTask(String processDefinitionId, String taskKey); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java new file mode 100644 index 00000000..506c6f15 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowCategoryService.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.*; + +import java.util.List; + +/** + * FlowCategory数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowCategoryService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowCategory 新增对象。 + * @return 返回新增对象。 + */ + FlowCategory saveNew(FlowCategory flowCategory); + + /** + * 更新数据对象。 + * + * @param flowCategory 更新的对象。 + * @param originalFlowCategory 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(FlowCategory flowCategory, FlowCategory originalFlowCategory); + + /** + * 删除指定数据。 + * + * @param categoryId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long categoryId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowCategoryListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowCategoryList(FlowCategory filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowCategoryList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowCategoryListWithRelation(FlowCategory filter, String orderBy); + + /** + * 当前流程分类编码是否存在。 + * + * @param code 流程分类编码。 + * @return true存在,否则false。 + */ + boolean existByCode(String code); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java new file mode 100644 index 00000000..9cd3a366 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryService.java @@ -0,0 +1,133 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.*; + +import javax.xml.stream.XMLStreamException; +import java.util.List; +import java.util.Set; + +/** + * FlowEntry数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowEntry 新增工作流对象。 + * @return 返回新增对象。 + */ + FlowEntry saveNew(FlowEntry flowEntry); + + /** + * 发布指定流程。 + * + * @param flowEntry 待发布的流程对象。 + * @param initTaskInfo 第一个非开始节点任务的附加信息。 + * @throws XMLStreamException 解析bpmn.xml的异常。 + */ + void publish(FlowEntry flowEntry, String initTaskInfo) throws XMLStreamException; + + /** + * 更新数据对象。 + * + * @param flowEntry 更新的对象。 + * @param originalFlowEntry 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(FlowEntry flowEntry, FlowEntry originalFlowEntry); + + /** + * 删除指定数据。 + * + * @param entryId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long entryId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowEntryListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryList(FlowEntry filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowEntryList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryListWithRelation(FlowEntry filter, String orderBy); + + /** + * 根据流程定义标识获取流程对象。从缓存中读取,如不存在则从数据库读取后,再同步到缓存。 + * + * @param processDefinitionKey 流程定义标识。 + * @return 流程对象。 + */ + FlowEntry getFlowEntryFromCache(String processDefinitionKey); + + /** + * 根据流程Id获取流程发布列表数据。 + * + * @param entryId 流程Id。 + * @return 流程关联的发布列表数据。 + */ + List getFlowEntryPublishList(Long entryId); + + /** + * 根据流程引擎中的流程定义Id集合,查询流程发布对象。 + * + * @param processDefinitionIdSet 流程引擎中的流程定义Id集合。 + * @return 查询结果。 + */ + List getFlowEntryPublishList(Set processDefinitionIdSet); + + /** + * 获取指定工作流发布版本对象。从缓存中读取,如缓存中不存在,从数据库读取并同步缓存。 + * + * @param entryPublishId 工作流发布对象Id。 + * @return 查询后的对象。 + */ + FlowEntryPublish getFlowEntryPublishFromCache(Long entryPublishId); + + /** + * 为指定工作流更新发布的主版本。 + * + * @param flowEntry 工作流对象。 + * @param newMainFlowEntryPublish 工作流新的发布主版本对象。 + */ + void updateFlowEntryMainVersion(FlowEntry flowEntry, FlowEntryPublish newMainFlowEntryPublish); + + /** + * 挂起指定的工作流发布对象。 + * + * @param flowEntryPublish 待挂起的工作流发布对象。 + */ + void suspendFlowEntryPublish(FlowEntryPublish flowEntryPublish); + + /** + * 激活指定的工作流发布对象。 + * + * @param flowEntryPublish 待恢复的工作流发布对象。 + */ + void activateFlowEntryPublish(FlowEntryPublish flowEntryPublish); + + /** + * 判断指定流程定义标识是否存在。 + * @param processDefinitionKey 流程定义标识。 + * @return true存在,否则false。 + */ + boolean existByProcessDefinitionKey(String processDefinitionKey); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java new file mode 100644 index 00000000..963a33fb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowEntryVariableService.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 流程变量数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowEntryVariableService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowEntryVariable 新增对象。 + * @return 返回新增对象。 + */ + FlowEntryVariable saveNew(FlowEntryVariable flowEntryVariable); + + /** + * 更新数据对象。 + * + * @param flowEntryVariable 更新的对象。 + * @param originalFlowEntryVariable 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(FlowEntryVariable flowEntryVariable, FlowEntryVariable originalFlowEntryVariable); + + /** + * 删除指定数据。 + * + * @param variableId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long variableId); + + /** + * 删除指定流程Id的所有变量。 + * + * @param entryId 流程Id。 + */ + void removeByEntryId(Long entryId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowEntryVariableListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryVariableList(FlowEntryVariable filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowEntryVariableList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowEntryVariableListWithRelation(FlowEntryVariable filter, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java new file mode 100644 index 00000000..1d0b53f8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMessageService.java @@ -0,0 +1,106 @@ +package com.orangeforms.common.flow.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.FlowMessage; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import org.flowable.task.api.Task; + +import java.util.List; + +/** + * 工作流消息数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMessageService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowMessage 新增对象。 + * @return 保存后的消息对象。 + */ + FlowMessage saveNew(FlowMessage flowMessage); + + /** + * 根据工单参数,保存催单消息对象。如果当前工单存在多个待办任务,则插入多条催办消息数据。 + * + * @param flowWorkOrder 待催办的工单。 + */ + void saveNewRemindMessage(FlowWorkOrder flowWorkOrder); + + /** + * 保存抄送消息对象。 + * + * @param task 待抄送的任务。 + * @param copyDataJson 抄送人员或者组的Id数据。 + */ + void saveNewCopyMessage(Task task, JSONObject copyDataJson); + + /** + * 更新指定运行时任务Id的消费为已完成状态。 + * + * @param taskId 运行时任务Id。 + */ + void updateFinishedStatusByTaskId(String taskId); + + /** + * 更新指定流程实例Id的消费为已完成状态。 + * + * @param processInstanceId 流程实例IdId。 + */ + void updateFinishedStatusByProcessInstanceId(String processInstanceId); + + /** + * 获取当前用户的催办消息列表。 + * + * @return 查询后的催办消息列表。 + */ + List getRemindingMessageListByUser(); + + /** + * 获取当前用户的抄送消息列表。 + * + * @param read true表示已读,false表示未读。 + * @return 查询后的抄送消息列表。 + */ + List getCopyMessageListByUser(Boolean read); + + /** + * 判断当前用户是否有权限访问指定消息Id。 + * + * @param messageId 消息Id。 + * @return true为合法访问者,否则false。 + */ + boolean isCandidateIdentityOnMessage(Long messageId); + + /** + * 读取抄送消息,同时更新当前用户对指定抄送消息的读取状态。 + * + * @param messageId 消息Id。 + */ + void readCopyTask(Long messageId); + + /** + * 计算当前用户催办消息的数量。 + * + * @return 当前用户催办消息数量。 + */ + int countRemindingMessageListByUser(); + + /** + * 计算当前用户未读抄送消息的数量。 + * + * @return 当前用户未读抄送消息数量。 + */ + int countCopyMessageByUser(); + + /** + * 删除指定流程实例的消息。 + * + * @param processInstanceId 流程实例Id。 + */ + void removeByProcessInstanceId(String processInstanceId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java new file mode 100644 index 00000000..3b0ff74c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowMultiInstanceTransService.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.FlowMultiInstanceTrans; + +/** + * 会签任务操作流水数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowMultiInstanceTransService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowMultiInstanceTrans 新增对象。 + * @return 返回新增对象。 + */ + FlowMultiInstanceTrans saveNew(FlowMultiInstanceTrans flowMultiInstanceTrans); + + /** + * 根据流程执行Id获取对象。 + * + * @param executionId 流程执行Id。 + * @param taskId 执行任务Id。 + * @return 数据对象。 + */ + FlowMultiInstanceTrans getByExecutionId(String executionId, String taskId); + + /** + * 根据多实例的统一执行Id,获取assgineeList字段不为空的数据。 + * + * @param multiInstanceExecId 多实例统一执行Id。 + * @return 数据对象。 + */ + FlowMultiInstanceTrans getWithAssigneeListByMultiInstanceExecId(String multiInstanceExecId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java new file mode 100644 index 00000000..4dc05e05 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskCommentService.java @@ -0,0 +1,84 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.flow.model.FlowTaskComment; + +import java.util.List; +import java.util.Set; + +/** + * 流程任务批注数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskCommentService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param flowTaskComment 新增对象。 + * @return 返回新增对象。 + */ + FlowTaskComment saveNew(FlowTaskComment flowTaskComment); + + /** + * 查询指定流程实例Id下的所有审批任务的批注。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果集。 + */ + List getFlowTaskCommentList(String processInstanceId); + + /** + * 查询与指定流程任务Id集合关联的所有审批任务的批注。 + * + * @param taskIdSet 流程任务Id集合。 + * @return 查询结果集。 + */ + List getFlowTaskCommentListByTaskIds(Set taskIdSet); + + /** + * 获取指定流程实例的最后一条审批任务。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果。 + */ + FlowTaskComment getLatestFlowTaskComment(String processInstanceId); + + /** + * 获取指定流程实例和任务定义标识的最后一条审批任务。 + * + * @param processInstanceId 流程实例Id。 + * @param taskDefinitionKey 任务定义标识。 + * @return 查询结果。 + */ + FlowTaskComment getLatestFlowTaskComment(String processInstanceId, String taskDefinitionKey); + + /** + * 获取指定流程实例的第一条审批任务。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果。 + */ + FlowTaskComment getFirstFlowTaskComment(String processInstanceId); + + /** + * 获取指定任务实例和执行批次的审批数据列表。 + * + * @param processInstanceId 流程实例。 + * @param taskId 任务Id + * @param executionId 任务执行Id + * @return 审批数据列表。 + */ + List getFlowTaskCommentListByExecutionId( + String processInstanceId, String taskId, String executionId); + + /** + * 根据多实例执行Id获取任务审批对象数据列表。 + * + * @param multiInstanceExecId 多实例执行Id。 + * @return 审批数据列表。 + */ + List getFlowTaskCommentListByMultiInstanceExecId(String multiInstanceExecId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java new file mode 100644 index 00000000..dca29c00 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowTaskExtService.java @@ -0,0 +1,124 @@ +package com.orangeforms.common.flow.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.object.FlowElementExtProperty; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.common.core.base.service.IBaseService; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.ExtensionElement; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.task.api.TaskInfo; + +import java.util.List; +import java.util.Map; + +/** + * 流程任务扩展数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowTaskExtService extends IBaseService { + + /** + * 批量插入流程任务扩展信息列表。 + * + * @param flowTaskExtList 流程任务扩展信息列表。 + */ + void saveBatch(List flowTaskExtList); + + /** + * 查询指定的流程任务扩展对象。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @param taskId 流程引擎的任务Id。 + * @return 查询结果。 + */ + FlowTaskExt getByProcessDefinitionIdAndTaskId(String processDefinitionId, String taskId); + + /** + * 查询指定的流程定义的任务扩展对象。 + * + * @param processDefinitionId 流程引擎的定义Id。 + * @return 查询结果。 + */ + List getByProcessDefinitionId(String processDefinitionId); + + /** + * 获取任务扩展信息中的候选人用户信息列表。 + * + * @param processInstanceId 流程引擎的实例Id。 + * @param flowTaskExt 任务扩展对象。 + * @param taskInfo 任务信息。 + * @param isMultiInstanceTask 是否为多实例任务。 + * @param historic 是否为历史任务。 + * @return 候选人用户信息列表。 + */ + List getCandidateUserInfoList( + String processInstanceId, + FlowTaskExt flowTaskExt, + TaskInfo taskInfo, + boolean isMultiInstanceTask, + boolean historic); + + /** + * 获取指定任务的用户列表信息。 + * + * @param processInstanceId 流程实例。 + * @param executionId 执行实例。 + * @param flowTaskExt 流程用户任务的扩展对象。 + * @return 候选人用户信息列表。 + */ + List getCandidateUserInfoList( + String processInstanceId, + String executionId, + FlowTaskExt flowTaskExt); + + /** + * 通过UserTask对象中的扩展节点信息,构建FLowTaskExt对象。 + * + * @param userTask 流程图中定义的用户任务对象。 + * @return 构建后的流程任务扩展信息对象。 + */ + FlowTaskExt buildTaskExtByUserTask(UserTask userTask); + + /** + * 获取指定流程图中所有UserTask对象的扩展节点信息,构建FLowTaskExt对象列表。 + * + * @param bpmnModel 流程图模型对象。 + * @return 当前流程图中所有用户流程任务的扩展信息对象列表。 + */ + List buildTaskExtList(BpmnModel bpmnModel); + + /** + * 根据流程定义中用户任务的扩展节点数据,构建出前端所需的操作列表数据对象。 + * @param extensionMap 用户任务的扩展节点。 + * @return 前端所需的操作列表数据对象。 + */ + List buildOperationListExtensionElement(Map> extensionMap); + + /** + * 根据流程定义中用户任务的扩展节点数据,构建出前端所需的变量列表数据对象。 + * @param extensionMap 用户任务的扩展节点。 + * @return 前端所需的变量列表数据对象。 + */ + List buildVariableListExtensionElement(Map> extensionMap); + + /** + * 读取流程定义中,流程元素的扩展属性数据。 + * + * @param element 流程图中定义的流程元素。 + * @return 流程元素的扩展属性数据。 + */ + FlowElementExtProperty buildFlowElementExt(FlowElement element); + + /** + * 读取流程定义中,流程元素的扩展属性数据。 + * + * @param element 流程图中定义的流程元素。 + * @return 流程元素的扩展属性数据,并转换为JSON对象。 + */ + JSONObject buildFlowElementExtToJson(FlowElement element); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java new file mode 100644 index 00000000..4299abe7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/FlowWorkOrderService.java @@ -0,0 +1,184 @@ +package com.orangeforms.common.flow.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyOrderParam; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.FlowWorkOrderExt; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import org.flowable.engine.runtime.ProcessInstance; + +import java.util.*; + +/** + * 工作流工单表数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface FlowWorkOrderService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param instance 流程实例对象。 + * @param dataId 流程实例的BusinessKey。 + * @param onlineTableId 在线数据表的主键Id。 + * @param tableName 面向静态表单所使用的表名。 + * @return 新增的工作流工单对象。 + */ + FlowWorkOrder saveNew(ProcessInstance instance, Object dataId, Long onlineTableId, String tableName); + + /** + * 保存工单草稿。 + * + * @param instance 流程实例对象。 + * @param onlineTableId 在线表单的主表Id。 + * @param tableName 静态表单的主表表名。 + * @param masterData 主表数据。 + * @param slaveData 从表数据。 + * @return 工单对象。 + */ + FlowWorkOrder saveNewWithDraft( + ProcessInstance instance, Long onlineTableId, String tableName, String masterData, String slaveData); + + /** + * 更新流程工单的草稿数据。 + * + * @param workOrderId 工单Id。 + * @param masterData 主表数据。 + * @param slaveData 从表数据。 + */ + void updateDraft(Long workOrderId, String masterData, String slaveData); + + /** + * 删除指定数据。 + * + * @param workOrderId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long workOrderId); + + /** + * 删除指定流程实例Id的关联工单。 + * + * @param processInstanceId 流程实例Id。 + */ + void removeByProcessInstanceId(String processInstanceId); + + /** + * 获取工作流工单单表查询结果。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowWorkOrderList(FlowWorkOrder filter, String orderBy); + + /** + * 获取工作流工单列表及其关联字典数据。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getFlowWorkOrderListWithRelation(FlowWorkOrder filter, String orderBy); + + /** + * 根据流程实例Id,查询关联的工单对象。 + * + * @param processInstanceId 流程实例Id。 + * @return 工作流工单对象。 + */ + FlowWorkOrder getFlowWorkOrderByProcessInstanceId(String processInstanceId); + + /** + * 根据业务主键,查询是否存在指定的工单。 + * + * @param tableName 静态表单工作流使用的数据表。 + * @param businessKey 业务数据主键Id。 + * @param unfinished 是否为没有结束工单。 + * @return 存在返回true,否则false。 + */ + boolean existByBusinessKey(String tableName, Object businessKey, boolean unfinished); + + /** + * 根据流程定义和业务主键,查询是否存在指定的未完成工单。 + * + * @param processDefinitionKey 静态表单工作流使用的数据表。 + * @param businessKey 业务数据主键Id。 + * @return 存在返回true,否则false。 + */ + boolean existUnfinished(String processDefinitionKey, Object businessKey); + + /** + * 根据流程实例Id,更新流程状态。 + * + * @param processInstanceId 流程实例Id。 + * @param flowStatus 新的流程状态值,如果该值为null,不执行任何更新。 + */ + void updateFlowStatusByProcessInstanceId(String processInstanceId, Integer flowStatus); + + /** + * 根据流程实例Id,更新流程最后审批状态。 + * + * @param processInstanceId 流程实例Id。 + * @param approvalStatus 新的流程最后审批状态,如果该值为null,不执行任何更新。 + */ + void updateLatestApprovalStatusByProcessInstanceId(String processInstanceId, Integer approvalStatus); + + /** + * 是否有查看该工单的数据权限。 + * + * @param processInstanceId 流程实例Id。 + * @return 存在返回true,否则false。 + */ + boolean hasDataPermOnFlowWorkOrder(String processInstanceId); + + /** + * 根据工单列表中的submitUserName,找到映射的userShowName,并会写到Vo中指定字段。 + * 同时这也是一个如何通过插件方法,将loginName映射到showName的示例, + * + * @param dataList 工单Vo对象列表。 + */ + void fillUserShowNameByLoginName(List dataList); + + /** + * 根据工单Id获取工单扩展对象数据。 + * + * @param workOrderId 工单Id。 + * @return 工单扩展对象。 + */ + FlowWorkOrderExt getFlowWorkOrderExtByWorkOrderId(Long workOrderId); + + /** + * 根据工单Id集合获取工单扩展对象数据列表。 + * + * @param workOrderIds 工单Id集合。 + * @return 工单扩展对象列表。 + */ + List getFlowWorkOrderExtByWorkOrderIds(Set workOrderIds); + + /** + * 移除草稿工单,同时停止已经启动的流程实例。 + * + * @param flowWorkOrder 工单对象。 + * @return 停止流程实例的结果。 + */ + CallResult removeDraft(FlowWorkOrder flowWorkOrder); + + /** + * 获取分页后的工单列表同时构建部分任务数据。该方法主要是为了尽量减少路由表单工作流listWorkOrder的重复代码。 + * + * @param filter 工单过滤对象。 + * @param pageParam 分页参数对象。 + * @param orderParam 排序参数对象。 + * @param processDefinitionKey 流程定义标识。 + * @return 分页的工单列表。 + */ + MyPageData getPagedWorkOrderListAndBuildData( + FlowWorkOrderDto filter, MyPageParam pageParam, MyOrderParam orderParam, String processDefinitionKey); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java new file mode 100644 index 00000000..7699f795 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowApiServiceImpl.java @@ -0,0 +1,2039 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MultiDatabaseWriteMethod; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.UserFilterGroup; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyDateUtil; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.exception.FlowOperationException; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.object.FlowEntryExtensionData; +import com.orangeforms.common.flow.object.FlowTaskMultiSignAssign; +import com.orangeforms.common.flow.object.FlowTaskOperation; +import com.orangeforms.common.flow.object.FlowTaskPostCandidateGroup; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.CustomChangeActivityStateBuilderImpl; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.*; +import org.flowable.common.engine.impl.de.odysseus.el.ExpressionFactoryImpl; +import org.flowable.common.engine.impl.de.odysseus.el.util.SimpleContext; +import org.flowable.common.engine.impl.identity.Authentication; +import org.flowable.common.engine.impl.javax.el.ExpressionFactory; +import org.flowable.common.engine.impl.javax.el.ValueExpression; +import org.flowable.engine.*; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.history.HistoricProcessInstanceQuery; +import org.flowable.engine.impl.RuntimeServiceImpl; +import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; +import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ChangeActivityStateBuilder; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.runtime.ProcessInstanceBuilder; +import org.flowable.identitylink.api.IdentityLink; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.TaskQuery; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.flowable.task.api.history.HistoricTaskInstanceQuery; +import org.flowable.variable.api.history.HistoricVariableInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowApiService") +public class FlowApiServiceImpl implements FlowApiService { + + @Autowired + private RepositoryService repositoryService; + @Autowired + private RuntimeService runtimeService; + @Autowired + private TaskService taskService; + @Autowired + private HistoryService historyService; + @Autowired + private ManagementService managementService; + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowTaskCommentService flowTaskCommentService; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowMessageService flowMessageService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowMultiInstanceTransService flowMultiInstanceTransService; + + @Transactional(rollbackFor = Exception.class) + @Override + public ProcessInstance start(String processDefinitionId, Object dataId) { + TokenData tokenData = TokenData.takeFromRequest(); + Map variableMap = this.initAndGetProcessInstanceVariables(processDefinitionId); + Authentication.setAuthenticatedUserId(tokenData.getLoginName()); + String businessKey = dataId == null ? null : dataId.toString(); + ProcessInstanceBuilder builder = runtimeService.createProcessInstanceBuilder() + .processDefinitionId(processDefinitionId).businessKey(businessKey).variables(variableMap); + if (tokenData.getTenantId() != null) { + builder.tenantId(tokenData.getTenantId().toString()); + } else { + if (tokenData.getAppCode() != null) { + builder.tenantId(tokenData.getAppCode()); + } + } + return builder.start(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Task takeFirstTask(String processInstanceId, FlowTaskComment flowTaskComment, JSONObject taskVariableData) { + String loginName = TokenData.takeFromRequest().getLoginName(); + // 获取流程启动后的第一个任务。 + Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).active().singleResult(); + if (StrUtil.equalsAny(task.getAssignee(), loginName, FlowConstant.START_USER_NAME_VAR)) { + // 按照规则,调用该方法的用户,就是第一个任务的assignee,因此默认会自动执行complete。 + flowTaskComment.fillWith(task); + this.completeTask(task, flowTaskComment, taskVariableData); + } + return task; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public ProcessInstance startAndTakeFirst( + String processDefinitionId, Object dataId, FlowTaskComment flowTaskComment, JSONObject taskVariableData) { + ProcessInstance instance = this.start(processDefinitionId, dataId); + this.takeFirstTask(instance.getProcessInstanceId(), flowTaskComment, taskVariableData); + return instance; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void submitConsign( + HistoricTaskInstance startTaskInstance, Task multiInstanceActiveTask, String newAssignees, boolean isAdd) { + JSONArray assigneeArray = JSON.parseArray(newAssignees); + String multiInstanceExecId = this.getExecutionVariableStringWithSafe( + multiInstanceActiveTask.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + FlowMultiInstanceTrans trans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + Set assigneeSet = new HashSet<>(StrUtil.split(trans.getAssigneeList(), ",")); + Task runtimeTask = null; + for (int i = 0; i < assigneeArray.size(); i++) { + String assignee = assigneeArray.getString(i); + if (isAdd) { + assigneeSet.add(assignee); + } else { + assigneeSet.remove(assignee); + } + if (isAdd) { + Map variables = new HashMap<>(2); + variables.put("assignee", assigneeArray.getString(i)); + variables.put(FlowConstant.MULTI_SIGN_START_TASK_VAR, startTaskInstance.getId()); + runtimeService.addMultiInstanceExecution( + multiInstanceActiveTask.getTaskDefinitionKey(), multiInstanceActiveTask.getProcessInstanceId(), variables); + } else { + TaskQuery query = taskService.createTaskQuery().active(); + query.processInstanceId(multiInstanceActiveTask.getProcessInstanceId()); + query.taskDefinitionKey(multiInstanceActiveTask.getTaskDefinitionKey()); + query.taskAssignee(assignee); + runtimeTask = query.singleResult(); + if (runtimeTask == null) { + throw new FlowOperationException("审批人 [" + assignee + "] 已经提交审批,不能执行减签操作!"); + } + runtimeService.deleteMultiInstanceExecution(runtimeTask.getExecutionId(), false); + } + } + if (!isAdd && runtimeTask != null) { + this.doChangeTask(runtimeTask); + } + trans.setAssigneeList(StrUtil.join(",", assigneeSet)); + flowMultiInstanceTransService.updateById(trans); + FlowTaskComment flowTaskComment = new FlowTaskComment(); + flowTaskComment.fillWith(startTaskInstance); + flowTaskComment.setApprovalType(isAdd ? FlowApprovalType.MULTI_CONSIGN : FlowApprovalType.MULTI_MINUS_SIGN); + String showName = TokenData.takeFromRequest().getLoginName(); + String comment = String.format("用户 [%s] [%s] [%s]。", isAdd ? "加签" : "减签", showName, newAssignees); + flowTaskComment.setTaskComment(comment); + flowTaskCommentService.saveNew(flowTaskComment); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void completeTask(Task task, FlowTaskComment flowTaskComment, JSONObject taskVariableData) { + if (taskVariableData == null) { + taskVariableData = new JSONObject(); + } + JSONObject passCopyData = (JSONObject) taskVariableData.remove(FlowConstant.COPY_DATA_KEY); + // 判断当前完成执行的任务,是否存在抄送设置。 + Object copyData = runtimeService.getVariable( + task.getProcessInstanceId(), FlowConstant.COPY_DATA_MAP_PREFIX + task.getTaskDefinitionKey()); + if (copyData != null || passCopyData != null) { + JSONObject copyDataJson = this.mergeCopyData(copyData, passCopyData); + flowMessageService.saveNewCopyMessage(task, copyDataJson); + } + if (flowTaskComment != null) { + // 这里处理多实例会签逻辑。 + if (flowTaskComment.getApprovalType().equals(FlowApprovalType.MULTI_SIGN)) { + String loginName = TokenData.takeFromRequest().getLoginName(); + String assigneeList = this.getMultiInstanceAssigneeList(task, taskVariableData); + Assert.isTrue(StrUtil.isNotBlank(assigneeList)); + taskVariableData.put(FlowConstant.MULTI_AGREE_COUNT_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_REFUSE_COUNT_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_ABSTAIN_COUNT_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_SIGN_NUM_OF_INSTANCES_VAR, 0); + taskVariableData.put(FlowConstant.MULTI_SIGN_START_TASK_VAR, task.getId()); + String multiInstanceExecId = MyCommonUtil.generateUuid(); + taskVariableData.put(FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR, multiInstanceExecId); + String comment = String.format("用户 [%s] 会签 [%s]。", loginName, assigneeList); + FlowMultiInstanceTrans multiInstanceTrans = new FlowMultiInstanceTrans(task); + multiInstanceTrans.setMultiInstanceExecId(multiInstanceExecId); + multiInstanceTrans.setAssigneeList(assigneeList); + flowMultiInstanceTransService.saveNew(multiInstanceTrans); + flowTaskComment.setTaskComment(comment); + } + // 处理转办。 + if (FlowApprovalType.TRANSFER.equals(flowTaskComment.getApprovalType())) { + this.transferTo(task, flowTaskComment); + return; + } + this.handleMultiInstanceApprovalType( + task.getExecutionId(), flowTaskComment.getApprovalType(), taskVariableData); + taskVariableData.put(FlowConstant.OPERATION_TYPE_VAR, flowTaskComment.getApprovalType()); + this.setSubmitUserVar(taskVariableData, flowTaskComment); + flowTaskComment.fillWith(task); + if (this.isMultiInstanceTask(task.getProcessDefinitionId(), task.getTaskDefinitionKey())) { + String multiInstanceExecId = getExecutionVariableStringWithSafe( + task.getExecutionId(), FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + FlowMultiInstanceTrans multiInstanceTrans = new FlowMultiInstanceTrans(task); + multiInstanceTrans.setMultiInstanceExecId(multiInstanceExecId); + flowMultiInstanceTransService.saveNew(multiInstanceTrans); + flowTaskComment.setMultiInstanceExecId(multiInstanceExecId); + } + flowTaskCommentService.saveNew(flowTaskComment); + } + taskVariableData.remove(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR); + Integer approvalStatus = MapUtil.getInt(taskVariableData, FlowConstant.LATEST_APPROVAL_STATUS_KEY); + flowWorkOrderService.updateLatestApprovalStatusByProcessInstanceId(task.getProcessInstanceId(), approvalStatus); + taskService.complete(task.getId(), taskVariableData, this.makeTransientVariableMap(taskVariableData)); + flowMessageService.updateFinishedStatusByTaskId(task.getId()); + } + + private void setSubmitUserVar(JSONObject taskVariableData, FlowTaskComment comment) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData != null) { + taskVariableData.put(FlowConstant.SUBMIT_USER_VAR, tokenData.getLoginName()); + } else { + if (StrUtil.isNotBlank(comment.getCreateLoginName())) { + taskVariableData.put(FlowConstant.SUBMIT_USER_VAR, comment.getCreateLoginName()); + } + } + } + + private JSONObject makeTransientVariableMap(JSONObject taskVariableData) { + JSONObject result = new JSONObject(); + if (taskVariableData == null) { + return result; + } + Object masterData = taskVariableData.get(FlowConstant.MASTER_DATA_KEY); + if (masterData != null) { + result.put(FlowConstant.MASTER_DATA_KEY, masterData); + } + Object slaveData = taskVariableData.get(FlowConstant.SLAVE_DATA_KEY); + if (slaveData != null) { + result.put(FlowConstant.SLAVE_DATA_KEY, slaveData); + } + Object masterTable = taskVariableData.get(FlowConstant.MASTER_TABLE_KEY); + if (masterTable != null) { + result.put(FlowConstant.MASTER_TABLE_KEY, masterTable); + } + taskVariableData.remove(FlowConstant.MASTER_DATA_KEY); + taskVariableData.remove(FlowConstant.SLAVE_DATA_KEY); + taskVariableData.remove(FlowConstant.MASTER_TABLE_KEY); + return result; + } + + private String getMultiInstanceAssigneeList(Task task, JSONObject taskVariableData) { + JSONArray assigneeArray = taskVariableData.getJSONArray(FlowConstant.MULTI_ASSIGNEE_LIST_VAR); + String assigneeList; + if (CollUtil.isEmpty(assigneeArray)) { + FlowTaskExt flowTaskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId( + task.getProcessDefinitionId(), task.getTaskDefinitionKey()); + assigneeList = this.buildMutiSignAssigneeList(flowTaskExt.getOperationListJson()); + if (assigneeList != null) { + taskVariableData.put(FlowConstant.MULTI_ASSIGNEE_LIST_VAR, StrUtil.split(assigneeList, ',')); + } + } else { + assigneeList = CollUtil.join(assigneeArray, ","); + } + return assigneeList; + } + + private JSONObject mergeCopyData(Object copyData, JSONObject passCopyData) { + // passCopyData是传阅数据,copyData是抄送数据。 + JSONObject resultCopyDataJson = passCopyData; + if (resultCopyDataJson == null) { + resultCopyDataJson = JSON.parseObject(copyData.toString()); + } else if (copyData != null) { + JSONObject copyDataJson = JSON.parseObject(copyData.toString()); + for (Map.Entry entry : copyDataJson.entrySet()) { + String value = resultCopyDataJson.getString(entry.getKey()); + if (value == null) { + resultCopyDataJson.put(entry.getKey(), entry.getValue()); + } else { + List list1 = StrUtil.split(value, ","); + List list2 = StrUtil.split(entry.getValue().toString(), ","); + Set valueSet = new HashSet<>(list1); + valueSet.addAll(list2); + resultCopyDataJson.put(entry.getKey(), StrUtil.join(",", valueSet)); + } + } + } + this.processMergeCopyData(resultCopyDataJson); + return resultCopyDataJson; + } + + private void processMergeCopyData(JSONObject resultCopyDataJson) { + TokenData tokenData = TokenData.takeFromRequest(); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + for (Map.Entry entry : resultCopyDataJson.entrySet()) { + String type = entry.getKey(); + switch (type) { + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR: + Object upLeaderDeptPostId = + flowIdentityExtHelper.getUpLeaderDeptPostId(tokenData.getDeptId()); + entry.setValue(upLeaderDeptPostId); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR: + Object leaderDeptPostId = + flowIdentityExtHelper.getLeaderDeptPostId(tokenData.getDeptId()); + entry.setValue(leaderDeptPostId); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + Set selfPostIdSet = new HashSet<>(StrUtil.split(entry.getValue().toString(), ",")); + Map deptPostIdMap = + flowIdentityExtHelper.getDeptPostIdMap(tokenData.getDeptId(), selfPostIdSet); + String deptPostIdValues = ""; + if (deptPostIdMap != null) { + deptPostIdValues = StrUtil.join(",", deptPostIdMap.values()); + } + entry.setValue(deptPostIdValues); + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + Set siblingPostIdSet = new HashSet<>(StrUtil.split(entry.getValue().toString(), ",")); + Map siblingDeptPostIdMap = + flowIdentityExtHelper.getSiblingDeptPostIdMap(tokenData.getDeptId(), siblingPostIdSet); + String siblingDeptPostIdValues = ""; + if (siblingDeptPostIdMap != null) { + siblingDeptPostIdValues = StrUtil.join(",", siblingDeptPostIdMap.values()); + } + entry.setValue(siblingDeptPostIdValues); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + Set upPostIdSet = new HashSet<>(StrUtil.split(entry.getValue().toString(), ",")); + Map upDeptPostIdMap = + flowIdentityExtHelper.getUpDeptPostIdMap(tokenData.getDeptId(), upPostIdSet); + String upDeptPostIdValues = ""; + if (upDeptPostIdMap != null) { + upDeptPostIdValues = StrUtil.join(",", upDeptPostIdMap.values()); + } + entry.setValue(upDeptPostIdValues); + break; + default: + break; + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult verifyAssigneeOrCandidateAndClaim(Task task) { + String errorMessage; + String loginName = TokenData.takeFromRequest().getLoginName(); + // 这里必须先执行拾取操作,如果当前用户是候选人,特别是对于分布式场景,更是要先完成候选人的拾取。 + if (task.getAssignee() == null) { + // 没有指派人 + if (!this.isAssigneeOrCandidate(task)) { + errorMessage = "数据验证失败,当前用户不是该待办任务的候选人,请刷新后重试!"; + return CallResult.error(errorMessage); + } + // 作为候选人主动拾取任务。 + taskService.claim(task.getId(), loginName); + } else { + if (!task.getAssignee().equals(loginName)) { + errorMessage = "数据验证失败,当前用户不是该待办任务的指派人,请刷新后重试!"; + return CallResult.error(errorMessage); + } + } + return CallResult.ok(); + } + + @Override + public Map initAndGetProcessInstanceVariables(String processDefinitionId) { + TokenData tokenData = TokenData.takeFromRequest(); + String loginName = tokenData.getLoginName(); + // 设置流程变量。 + Map variableMap = new HashMap<>(4); + variableMap.put(FlowConstant.PROC_INSTANCE_INITIATOR_VAR, loginName); + variableMap.put(FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR, loginName); + List flowTaskExtList = flowTaskExtService.getByProcessDefinitionId(processDefinitionId); + boolean hasDeptPostLeader = false; + boolean hasUpDeptPostLeader = false; + boolean hasPostCandidateGroup = false; + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + hasUpDeptPostLeader = true; + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + hasDeptPostLeader = true; + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_POST)) { + hasPostCandidateGroup = true; + } + } + // 如果流程图的配置中包含用户身份相关的变量(如:部门领导和上级领导审批),flowIdentityExtHelper就不能为null。 + // 这个需要子类去实现 BaseFlowIdentityExtHelper 接口,并注册到FlowCustomExtFactory的工厂中。 + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + if (hasUpDeptPostLeader) { + Assert.notNull(flowIdentityExtHelper); + Object upLeaderDeptPostId = flowIdentityExtHelper.getUpLeaderDeptPostId(tokenData.getDeptId()); + if (upLeaderDeptPostId == null) { + variableMap.put(FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, null); + } else { + variableMap.put(FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, upLeaderDeptPostId.toString()); + } + } + if (hasDeptPostLeader) { + Assert.notNull(flowIdentityExtHelper); + Object leaderDeptPostId = flowIdentityExtHelper.getLeaderDeptPostId(tokenData.getDeptId()); + if (leaderDeptPostId == null) { + variableMap.put(FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, null); + } else { + variableMap.put(FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, leaderDeptPostId.toString()); + } + } + if (hasPostCandidateGroup) { + Assert.notNull(flowIdentityExtHelper); + Map postGroupDataMap = + this.buildPostCandidateGroupData(flowIdentityExtHelper, flowTaskExtList); + variableMap.putAll(postGroupDataMap); + } + this.buildCopyData(flowTaskExtList, variableMap); + return variableMap; + } + + private void buildCopyData(List flowTaskExtList, Map variableMap) { + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (StrUtil.isBlank(flowTaskExt.getCopyListJson())) { + continue; + } + List copyDataList = JSON.parseArray(flowTaskExt.getCopyListJson(), JSONObject.class); + Map copyDataMap = new HashMap<>(copyDataList.size()); + for (JSONObject copyData : copyDataList) { + String type = copyData.getString("type"); + String id = copyData.getString("id"); + copyDataMap.put(type, id == null ? "" : id); + } + variableMap.put(FlowConstant.COPY_DATA_MAP_PREFIX + flowTaskExt.getTaskId(), JSON.toJSONString(copyDataMap)); + } + } + + private Map buildPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, List flowTaskExtList) { + Map postVariableMap = MapUtil.newHashMap(); + Set selfPostIdSet = new HashSet<>(); + Set siblingPostIdSet = new HashSet<>(); + Set upPostIdSet = new HashSet<>(); + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (flowTaskExt.getGroupType().equals(FlowConstant.GROUP_TYPE_POST)) { + Assert.notNull(flowTaskExt.getDeptPostListJson()); + List groupDataList = + JSONArray.parseArray(flowTaskExt.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR -> selfPostIdSet.add(groupData.getPostId()); + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR -> siblingPostIdSet.add(groupData.getPostId()); + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR -> upPostIdSet.add(groupData.getPostId()); + default -> { + } + } + } + } + } + postVariableMap.putAll(this.buildSelfPostCandidateGroupData(flowIdentityExtHelper, selfPostIdSet)); + postVariableMap.putAll(this.buildSiblingPostCandidateGroupData(flowIdentityExtHelper, siblingPostIdSet)); + postVariableMap.putAll(this.buildUpPostCandidateGroupData(flowIdentityExtHelper, upPostIdSet)); + return postVariableMap; + } + + private Map buildSelfPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, Set selfPostIdSet) { + Map postVariableMap = MapUtil.newHashMap(); + if (CollUtil.isNotEmpty(selfPostIdSet)) { + Map deptPostIdMap = + flowIdentityExtHelper.getDeptPostIdMap(TokenData.takeFromRequest().getDeptId(), selfPostIdSet); + for (String postId : selfPostIdSet) { + if (MapUtil.isNotEmpty(deptPostIdMap) && deptPostIdMap.containsKey(postId)) { + String deptPostId = deptPostIdMap.get(postId); + postVariableMap.put(FlowConstant.SELF_DEPT_POST_PREFIX + postId, deptPostId); + } else { + postVariableMap.put(FlowConstant.SELF_DEPT_POST_PREFIX + postId, ""); + } + } + } + return postVariableMap; + } + + private Map buildSiblingPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, Set siblingPostIdSet) { + Map postVariableMap = MapUtil.newHashMap(); + if (CollUtil.isNotEmpty(siblingPostIdSet)) { + Map siblingDeptPostIdMap = + flowIdentityExtHelper.getSiblingDeptPostIdMap(TokenData.takeFromRequest().getDeptId(), siblingPostIdSet); + for (String postId : siblingPostIdSet) { + if (MapUtil.isNotEmpty(siblingDeptPostIdMap) && siblingDeptPostIdMap.containsKey(postId)) { + String siblingDeptPostId = siblingDeptPostIdMap.get(postId); + postVariableMap.put(FlowConstant.SIBLING_DEPT_POST_PREFIX + postId, siblingDeptPostId); + } else { + postVariableMap.put(FlowConstant.SIBLING_DEPT_POST_PREFIX + postId, ""); + } + } + } + return postVariableMap; + } + + private Map buildUpPostCandidateGroupData( + BaseFlowIdentityExtHelper flowIdentityExtHelper, Set upPostIdSet) { + Map postVariableMap = MapUtil.newHashMap(); + if (CollUtil.isNotEmpty(upPostIdSet)) { + Map upDeptPostIdMap = + flowIdentityExtHelper.getUpDeptPostIdMap(TokenData.takeFromRequest().getDeptId(), upPostIdSet); + for (String postId : upPostIdSet) { + if (MapUtil.isNotEmpty(upDeptPostIdMap) && upDeptPostIdMap.containsKey(postId)) { + String upDeptPostId = upDeptPostIdMap.get(postId); + postVariableMap.put(FlowConstant.UP_DEPT_POST_PREFIX + postId, upDeptPostId); + } else { + postVariableMap.put(FlowConstant.UP_DEPT_POST_PREFIX + postId, ""); + } + } + } + return postVariableMap; + } + + @Override + public boolean isAssigneeOrCandidate(TaskInfo task) { + String loginName = TokenData.takeFromRequest().getLoginName(); + if (StrUtil.isNotBlank(task.getAssignee())) { + return StrUtil.equals(loginName, task.getAssignee()); + } + TaskQuery query = taskService.createTaskQuery(); + this.buildCandidateCondition(query, loginName); + query.taskId(task.getId()); + return query.active().count() != 0; + } + + @Override + public Collection getProcessAllElements(String processDefinitionId) { + Process process = repositoryService.getBpmnModel(processDefinitionId).getProcesses().get(0); + return this.getAllElements(process.getFlowElements(), null); + } + + @Override + public boolean isProcessInstanceStarter(String processInstanceId) { + String loginName = TokenData.takeFromRequest().getLoginName(); + return historyService.createHistoricProcessInstanceQuery() + .processInstanceId(processInstanceId).startedBy(loginName).count() != 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void setBusinessKeyForProcessInstance(String processInstanceId, Object dataId) { + runtimeService.updateBusinessKey(processInstanceId, dataId.toString()); + } + + @Override + public boolean existActiveProcessInstance(String processInstanceId) { + return runtimeService.createProcessInstanceQuery() + .processInstanceId(processInstanceId).active().count() != 0; + } + + @Override + public ProcessInstance getProcessInstance(String processInstanceId) { + return runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); + } + + @Override + public ProcessInstance getProcessInstanceByBusinessKey(String processDefinitionId, String businessKey) { + return runtimeService.createProcessInstanceQuery() + .processDefinitionId(processDefinitionId).processInstanceBusinessKey(businessKey).singleResult(); + } + + @Override + public Task getProcessInstanceActiveTask(String processInstanceId, String taskId) { + TaskQuery query = taskService.createTaskQuery().processInstanceId(processInstanceId); + if (StrUtil.isNotBlank(taskId)) { + query.taskId(taskId); + } + return query.active().singleResult(); + } + + @Override + public List getProcessInstanceActiveTaskList(String processInstanceId) { + return taskService.createTaskQuery().processInstanceId(processInstanceId).list(); + } + + @Override + public List getProcessInstanceActiveTaskListAndConvert(String processInstanceId) { + List taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list(); + return this.convertToFlowTaskList(taskList); + } + + @Override + public Task getTaskById(String taskId) { + return taskService.createTaskQuery().taskId(taskId).singleResult(); + } + + @Override + public MyPageData getTaskListByUserName( + String username, String definitionKey, String definitionName, String taskName, MyPageParam pageParam) { + TaskQuery query = this.createQuery(); + if (StrUtil.isNotBlank(definitionKey)) { + query.processDefinitionKey(definitionKey); + } + if (StrUtil.isNotBlank(definitionName)) { + query.processDefinitionNameLike("%" + definitionName + "%"); + } + if (StrUtil.isNotBlank(taskName)) { + query.taskNameLike("%" + taskName + "%"); + } + this.buildCandidateCondition(query, username); + long totalCount = query.count(); + query.orderByTaskCreateTime().desc(); + int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + List taskList = query.listPage(firstResult, pageParam.getPageSize()); + return new MyPageData<>(taskList, totalCount); + } + + @Override + public long getTaskCountByUserName(String username) { + TaskQuery query = this.createQuery(); + this.buildCandidateCondition(query, username); + return query.count(); + } + + @Override + public List getTaskListByProcessInstanceIds(List processInstanceIdSet) { + return taskService.createTaskQuery().processInstanceIdIn(processInstanceIdSet).active().list(); + } + + @Override + public List getProcessInstanceList(Set processInstanceIdSet) { + return runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIdSet).list(); + } + + @Override + public ProcessDefinition getProcessDefinitionById(String processDefinitionId) { + return repositoryService.createProcessDefinitionQuery().processDefinitionId(processDefinitionId).singleResult(); + } + + @Override + public List getProcessDefinitionList(Set processDefinitionIdSet) { + return repositoryService.createProcessDefinitionQuery().processDefinitionIds(processDefinitionIdSet).list(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void suspendProcessDefinition(String processDefinitionId) { + repositoryService.suspendProcessDefinitionById(processDefinitionId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void activateProcessDefinition(String processDefinitionId) { + repositoryService.activateProcessDefinitionById(processDefinitionId); + } + + @Override + public BpmnModel getBpmnModelByDefinitionId(String processDefinitionId) { + return repositoryService.getBpmnModel(processDefinitionId); + } + + @Override + public boolean isMultiInstanceTask(String processDefinitionId, String taskKey) { + BpmnModel model = this.getBpmnModelByDefinitionId(processDefinitionId); + FlowElement flowElement = model.getFlowElement(taskKey); + if (!(flowElement instanceof UserTask userTask)) { + return false; + } + return userTask.hasMultiInstanceLoopCharacteristics(); + } + + @Override + public ProcessDefinition getProcessDefinitionByDeployId(String deployId) { + return repositoryService.createProcessDefinitionQuery().deploymentId(deployId).singleResult(); + } + + @Override + public void setProcessInstanceVariables(String processInstanceId, Map variableMap) { + runtimeService.setVariables(processInstanceId, variableMap); + } + + @Override + public Object getProcessInstanceVariable(String processInstanceId, String variableName) { + return runtimeService.getVariable(processInstanceId, variableName); + } + + @Override + public List convertToFlowTaskList(List taskList) { + List flowTaskVoList = new LinkedList<>(); + if (CollUtil.isEmpty(taskList)) { + return flowTaskVoList; + } + Set processDefinitionIdSet = taskList.stream() + .map(Task::getProcessDefinitionId).collect(Collectors.toSet()); + Set procInstanceIdSet = taskList.stream() + .map(Task::getProcessInstanceId).collect(Collectors.toSet()); + List flowEntryPublishList = + flowEntryService.getFlowEntryPublishList(processDefinitionIdSet); + Map flowEntryPublishMap = + flowEntryPublishList.stream().collect(Collectors.toMap(FlowEntryPublish::getProcessDefinitionId, c -> c)); + List instanceList = this.getProcessInstanceList(procInstanceIdSet); + Map instanceMap = + instanceList.stream().collect(Collectors.toMap(ProcessInstance::getId, c -> c)); + List definitionList = this.getProcessDefinitionList(processDefinitionIdSet); + Map definitionMap = + definitionList.stream().collect(Collectors.toMap(ProcessDefinition::getId, c -> c)); + List workOrderList = + flowWorkOrderService.getInList("processInstanceId", procInstanceIdSet); + Map workOrderMap = + workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + for (Task task : taskList) { + FlowTaskVo flowTaskVo = new FlowTaskVo(); + flowTaskVo.setTaskId(task.getId()); + flowTaskVo.setTaskName(task.getName()); + flowTaskVo.setTaskKey(task.getTaskDefinitionKey()); + flowTaskVo.setTaskFormKey(task.getFormKey()); + flowTaskVo.setTaskStartTime(task.getCreateTime()); + flowTaskVo.setEntryId(flowEntryPublishMap.get(task.getProcessDefinitionId()).getEntryId()); + ProcessDefinition processDefinition = definitionMap.get(task.getProcessDefinitionId()); + flowTaskVo.setProcessDefinitionId(processDefinition.getId()); + flowTaskVo.setProcessDefinitionName(processDefinition.getName()); + flowTaskVo.setProcessDefinitionKey(processDefinition.getKey()); + flowTaskVo.setProcessDefinitionVersion(processDefinition.getVersion()); + ProcessInstance processInstance = instanceMap.get(task.getProcessInstanceId()); + flowTaskVo.setProcessInstanceId(processInstance.getId()); + Object initiator = this.getProcessInstanceVariable( + processInstance.getId(), FlowConstant.PROC_INSTANCE_INITIATOR_VAR); + flowTaskVo.setProcessInstanceInitiator(initiator.toString()); + flowTaskVo.setProcessInstanceStartTime(processInstance.getStartTime()); + flowTaskVo.setBusinessKey(processInstance.getBusinessKey()); + FlowWorkOrder flowWorkOrder = workOrderMap.get(task.getProcessInstanceId()); + if (flowWorkOrder != null) { + flowTaskVo.setIsDraft(flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)); + flowTaskVo.setWorkOrderCode(flowWorkOrder.getWorkOrderCode()); + } + flowTaskVoList.add(flowTaskVo); + } + Set loginNameSet = flowTaskVoList.stream() + .map(FlowTaskVo::getProcessInstanceInitiator).collect(Collectors.toSet()); + List flowUserInfos = flowCustomExtFactory + .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); + Map userInfoMap = + flowUserInfos.stream().collect(Collectors.toMap(FlowUserInfoVo::getLoginName, c -> c)); + for (FlowTaskVo flowTaskVo : flowTaskVoList) { + FlowUserInfoVo userInfo = userInfoMap.get(flowTaskVo.getProcessInstanceInitiator()); + flowTaskVo.setShowName(userInfo.getShowName()); + flowTaskVo.setHeadImageUrl(userInfo.getHeadImageUrl()); + } + return flowTaskVoList; + } + + @Override + public void addProcessInstanceEndListener(BpmnModel bpmnModel, Class listenerClazz) { + Assert.notNull(listenerClazz); + Process process = bpmnModel.getMainProcess(); + FlowableListener listener = this.createListener("end", listenerClazz.getName()); + process.getExecutionListeners().add(listener); + } + + @Override + public void addExecutionListener( + FlowElement flowElement, + Class listenerClazz, + String event, + List fieldExtensions) { + Assert.notNull(listenerClazz); + FlowableListener listener = this.createListener(event, listenerClazz.getName()); + if (fieldExtensions != null) { + listener.setFieldExtensions(fieldExtensions); + } + flowElement.getExecutionListeners().add(listener); + } + + @Override + public void addTaskCreateListener(UserTask userTask, Class listenerClazz) { + Assert.notNull(listenerClazz); + FlowableListener listener = this.createListener("create", listenerClazz.getName()); + userTask.getTaskListeners().add(listener); + } + + @Override + public HistoricProcessInstance getHistoricProcessInstance(String processInstanceId) { + return historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); + } + + @Override + public List getHistoricProcessInstanceList(Set processInstanceIdSet) { + return historyService.createHistoricProcessInstanceQuery().processInstanceIds(processInstanceIdSet).list(); + } + + @Override + public MyPageData getHistoricProcessInstanceList( + String processDefinitionKey, + String processDefinitionName, + String startUser, + String beginDate, + String endDate, + MyPageParam pageParam, + boolean finishedOnly) throws ParseException { + HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery(); + if (StrUtil.isNotBlank(processDefinitionKey)) { + query.processDefinitionKey(processDefinitionKey); + } + if (StrUtil.isNotBlank(processDefinitionName)) { + query.processDefinitionName(processDefinitionName); + } + if (StrUtil.isNotBlank(startUser)) { + query.startedBy(startUser); + } + if (StrUtil.isNotBlank(beginDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.startedAfter(sdf.parse(beginDate)); + } + if (StrUtil.isNotBlank(endDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.startedBefore(sdf.parse(endDate)); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() != null) { + query.processInstanceTenantId(tokenData.getTenantId().toString()); + } else { + if (tokenData.getAppCode() == null) { + query.processInstanceWithoutTenantId(); + } else { + query.processInstanceTenantId(tokenData.getAppCode()); + } + } + if (finishedOnly) { + query.finished(); + } + query.orderByProcessInstanceStartTime().desc(); + long totalCount = query.count(); + int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + List instanceList = query.listPage(firstResult, pageParam.getPageSize()); + return new MyPageData<>(instanceList, totalCount); + } + + @Override + public MyPageData getHistoricTaskInstanceFinishedList( + String processDefinitionName, + String beginDate, + String endDate, + MyPageParam pageParam) throws ParseException { + String loginName = TokenData.takeFromRequest().getLoginName(); + HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery() + .taskAssignee(loginName) + .finished(); + if (StrUtil.isNotBlank(processDefinitionName)) { + query.processDefinitionName(processDefinitionName); + } + if (StrUtil.isNotBlank(beginDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.taskCompletedAfter(sdf.parse(beginDate)); + } + if (StrUtil.isNotBlank(endDate)) { + SimpleDateFormat sdf = new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + query.taskCompletedBefore(sdf.parse(endDate)); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() != null) { + query.taskTenantId(tokenData.getTenantId().toString()); + } else { + if (StrUtil.isBlank(tokenData.getAppCode())) { + query.taskWithoutTenantId(); + } else { + query.taskTenantId(tokenData.getAppCode()); + } + } + query.orderByHistoricTaskInstanceEndTime().desc(); + long totalCount = query.count(); + int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); + List instanceList = query.listPage(firstResult, pageParam.getPageSize()); + return new MyPageData<>(instanceList, totalCount); + } + + @Override + public List getHistoricActivityInstanceList(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list(); + } + + @Override + public List getHistoricActivityInstanceListOrderByStartTime(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list(); + } + + @Override + public HistoricTaskInstance getHistoricTaskInstance(String processInstanceId, String taskId) { + return historyService.createHistoricTaskInstanceQuery() + .processInstanceId(processInstanceId).taskId(taskId).singleResult(); + } + + @Override + public List getHistoricUnfinishedInstanceList(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId).unfinished().list(); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult stopProcessInstance(String processInstanceId, String stopReason, boolean forCancel) { + //需要先更新状态,以便FlowFinishedListener监听器可以正常的判断流程结束的状态。 + int status = FlowTaskStatus.STOPPED; + if (forCancel) { + status = FlowTaskStatus.CANCELLED; + } + return this.stopProcessInstance(processInstanceId, stopReason, status); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult stopProcessInstance(String processInstanceId, String stopReason, int status) { + List taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).active().list(); + if (CollUtil.isEmpty(taskList)) { + return CallResult.error("数据验证失败,当前流程尚未开始或已经结束!"); + } + BpmnModel bpmnModel = repositoryService.getBpmnModel(taskList.get(0).getProcessDefinitionId()); + EndEvent endEvent = bpmnModel.getMainProcess() + .findFlowElementsOfType(EndEvent.class, false).get(0); + List currentActivitiIds = new LinkedList<>(); + flowWorkOrderService.updateFlowStatusByProcessInstanceId(processInstanceId, status); + for (Task task : taskList) { + String currActivityId = task.getTaskDefinitionKey(); + currentActivitiIds.add(currActivityId); + FlowNode currFlow = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currActivityId); + if (currFlow == null) { + List subProcessList = + bpmnModel.getMainProcess().findFlowElementsOfType(SubProcess.class); + for (SubProcess subProcess : subProcessList) { + FlowElement flowElement = subProcess.getFlowElement(currActivityId); + if (flowElement != null) { + currFlow = (FlowNode) flowElement; + break; + } + } + } + org.springframework.util.Assert.notNull(currFlow, "currFlow can't be NULL"); + if (!(currFlow.getParentContainer().equals(endEvent.getParentContainer()))) { + throw new FlowOperationException("数据验证失败,不能从子流程直接中止!"); + } + FlowTaskComment taskComment = new FlowTaskComment(task); + taskComment.setApprovalType(FlowApprovalType.STOP); + taskComment.setTaskComment(stopReason); + flowTaskCommentService.saveNew(taskComment); + } + this.doChangeState(processInstanceId, currentActivitiIds, CollUtil.newArrayList(endEvent.getId())); + flowMessageService.updateFinishedStatusByProcessInstanceId(processInstanceId); + return CallResult.ok(); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteProcessInstance(String processInstanceId) { + historyService.deleteHistoricProcessInstance(processInstanceId); + flowMessageService.removeByProcessInstanceId(processInstanceId); + FlowWorkOrder workOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); + if (workOrder == null) { + return; + } + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(workOrder.getProcessDefinitionKey()); + if (StrUtil.isNotBlank(flowEntry.getExtensionData())) { + FlowEntryExtensionData extData = JSON.parseObject(flowEntry.getExtensionData(), FlowEntryExtensionData.class); + if (BooleanUtil.isTrue(extData.getCascadeDeleteBusinessData())) { + // 级联删除在线表单工作流的业务数据。 + flowCustomExtFactory.getOnlineBusinessDataExtHelper().deleteBusinessData(workOrder); + } + } + flowWorkOrderService.removeByProcessInstanceId(processInstanceId); + } + + @Override + public Object getTaskVariable(String taskId, String variableName) { + return taskService.getVariable(taskId, variableName); + } + + @Override + public String getTaskVariableStringWithSafe(String taskId, String variableName) { + try { + Object v = taskService.getVariable(taskId, variableName); + if (v == null) { + return null; + } + return v.toString(); + } catch (Exception e) { + String errorMessage = + String.format("Failed to getTaskVariable taskId [%s], variableName [%s]", taskId, variableName); + log.error(errorMessage, e); + return null; + } + } + + @Override + public Object getExecutionVariable(String executionId, String variableName) { + return runtimeService.getVariable(executionId, variableName); + } + + @Override + public String getExecutionVariableStringWithSafe(String executionId, String variableName) { + try { + Object v = runtimeService.getVariable(executionId, variableName); + if (v == null) { + return null; + } + return v.toString(); + } catch (Exception e) { + String errorMessage = String.format( + "Failed to getExecutionVariableStringWithSafe executionId [%s], variableName [%s]", executionId, variableName); + log.error(errorMessage, e); + return null; + } + } + + @Override + public Object getHistoricProcessInstanceVariable(String processInstanceId, String variableName) { + HistoricVariableInstance hv = historyService.createHistoricVariableInstanceQuery() + .processInstanceId(processInstanceId).variableName(variableName).singleResult(); + return hv == null ? null : hv.getValue(); + } + + @Override + public BpmnModel convertToBpmnModel(String bpmnXml) throws XMLStreamException { + BpmnXMLConverter converter = new BpmnXMLConverter(); + InputStream in = new ByteArrayInputStream(bpmnXml.getBytes(StandardCharsets.UTF_8)); + @Cleanup XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(in); + return converter.convertToBpmnModel(reader); + } + + @Transactional + @Override + public CallResult backToRuntimeTask(Task task, String targetKey, boolean forReject, String reason) { + String errorMessage; + ProcessDefinition processDefinition = this.getProcessDefinitionById(task.getProcessDefinitionId()); + Collection allElements = this.getProcessAllElements(processDefinition.getId()); + FlowElement source = null; + // 获取跳转的节点元素 + FlowElement target = null; + for (FlowElement flowElement : allElements) { + if (flowElement.getId().equals(task.getTaskDefinitionKey())) { + source = flowElement; + if (StrUtil.isBlank(targetKey)) { + break; + } + } + if (StrUtil.isNotBlank(targetKey)) { + if (flowElement.getId().equals(targetKey)) { + target = flowElement; + } + } + } + if (targetKey != null && target == null) { + errorMessage = "数据验证失败,被驳回的指定目标节点不存在!"; + return CallResult.error(errorMessage); + } + UserTask oneUserTask = null; + List targetIds = null; + if (target == null) { + List parentUserTaskList = this.getParentUserTaskList(source, null, null); + if (CollUtil.isEmpty(parentUserTaskList)) { + errorMessage = "数据验证失败,当前节点为初始任务节点,不能驳回!"; + return CallResult.error(errorMessage); + } + // 获取活动ID, 即节点Key + Set parentUserTaskKeySet = new HashSet<>(); + parentUserTaskList.forEach(item -> parentUserTaskKeySet.add(item.getId())); + List historicActivityIdList = + this.getHistoricActivityInstanceListOrderByStartTime(task.getProcessInstanceId()); + // 数据清洗,将回滚导致的脏数据清洗掉 + List lastHistoricTaskInstanceList = + this.cleanHistoricTaskInstance(allElements, historicActivityIdList); + // 此时历史任务实例为倒序,获取最后走的节点 + targetIds = new ArrayList<>(); + // 循环结束标识,遇到当前目标节点的次数 + int number = 0; + StringBuilder parentHistoricTaskKey = new StringBuilder(); + for (String historicTaskInstanceKey : lastHistoricTaskInstanceList) { + // 当会签时候会出现特殊的,连续都是同一个节点历史数据的情况,这种时候跳过 + if (parentHistoricTaskKey.toString().equals(historicTaskInstanceKey)) { + continue; + } + parentHistoricTaskKey = new StringBuilder(historicTaskInstanceKey); + if (historicTaskInstanceKey.equals(task.getTaskDefinitionKey())) { + number++; + } + if (number == 2) { + break; + } + // 如果当前历史节点,属于父级的节点,说明最后一次经过了这个点,需要退回这个点 + if (parentUserTaskKeySet.contains(historicTaskInstanceKey)) { + targetIds.add(historicTaskInstanceKey); + } + } + // 目的获取所有需要被跳转的节点 currentIds + // 取其中一个父级任务,因为后续要么存在公共网关,要么就是串行公共线路 + oneUserTask = parentUserTaskList.get(0); + } + // 获取所有正常进行的执行任务的活动节点ID,这些任务不能直接使用,需要找出其中需要撤回的任务 + List runExecutionList = + runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId()).list(); + List runActivityIdList = runExecutionList.stream() + .map(Execution::getActivityId) + .filter(StrUtil::isNotBlank).collect(Collectors.toList()); + // 需驳回任务列表 + List currentIds = new ArrayList<>(); + // 通过父级网关的出口连线,结合 runExecutionList 比对,获取需要撤回的任务 + List currentFlowElementList = this.getChildUserTaskList( + target != null ? target : oneUserTask, runActivityIdList, null, null); + currentFlowElementList.forEach(item -> currentIds.add(item.getId())); + if (target == null) { + // 规定:并行网关之前节点必须需存在唯一用户任务节点,如果出现多个任务节点,则并行网关节点默认为结束节点,原因为不考虑多对多情况 + if (targetIds.size() > 1 && currentIds.size() > 1) { + errorMessage = "数据验证失败,任务出现多对多情况,无法撤回!"; + return CallResult.error(errorMessage); + } + } + AtomicReference> tmp = new AtomicReference<>(); + // 用于下面新增网关删除信息时使用 + String targetTmp = targetKey != null ? targetKey : String.join(",", targetIds); + // currentIds 为活动ID列表 + // currentExecutionIds 为执行任务ID列表 + // 需要通过执行任务ID来设置驳回信息,活动ID不行 + currentIds.forEach(currentId -> runExecutionList.forEach(runExecution -> { + if (StrUtil.isNotBlank(runExecution.getActivityId()) && currentId.equals(runExecution.getActivityId())) { + // 查询当前节点的执行任务的历史数据 + tmp.set(historyService.createHistoricActivityInstanceQuery() + .processInstanceId(task.getProcessInstanceId()) + .executionId(runExecution.getId()) + .activityId(runExecution.getActivityId()).list()); + // 如果这个列表的数据只有 1 条数据 + // 网关肯定只有一条,且为包容网关或并行网关 + // 这里的操作目的是为了给网关在扭转前提前加上删除信息,结构与普通节点的删除信息一样,目的是为了知道这个网关也是有经过跳转的 + if (tmp.get() != null && tmp.get().size() == 1 && StrUtil.isNotBlank(tmp.get().get(0).getActivityType()) + && ("parallelGateway".equals(tmp.get().get(0).getActivityType()) || "inclusiveGateway".equals(tmp.get().get(0).getActivityType()))) { + // singleResult 能够执行更新操作 + // 利用 流程实例ID + 执行任务ID + 活动节点ID 来指定唯一数据,保证数据正确 + historyService.createNativeHistoricActivityInstanceQuery().sql( + "UPDATE ACT_HI_ACTINST SET DELETE_REASON_ = 'Change activity to " + targetTmp + "' WHERE PROC_INST_ID_='" + task.getProcessInstanceId() + "' AND EXECUTION_ID_='" + runExecution.getId() + "' AND ACT_ID_='" + runExecution.getActivityId() + "'").singleResult(); + } + } + })); + try { + if (StrUtil.isNotBlank(targetKey)) { + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveActivityIdsToSingleActivityId(currentIds, targetKey).changeState(); + } else { + // 如果父级任务多于 1 个,说明当前节点不是并行节点,原因为不考虑多对多情况 + if (targetIds.size() > 1) { + // 1 对 多任务跳转,currentIds 当前节点(1),targetIds 跳转到的节点(多) + ChangeActivityStateBuilder builder = runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveSingleActivityIdToActivityIds(currentIds.get(0), targetIds); + for (String targetId : targetIds) { + FlowTaskComment taskComment = + flowTaskCommentService.getLatestFlowTaskComment(task.getProcessInstanceId(), targetId); + // 如果驳回后的目标任务包含指定人,则直接通过变量回抄,如果没有则自动忽略该变量,不会给流程带来任何影响。 + String submitLoginName = taskComment.getCreateLoginName(); + if (StrUtil.isNotBlank(submitLoginName)) { + builder.localVariable(targetId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR, submitLoginName); + } + } + builder.changeState(); + } + // 如果父级任务只有一个,因此当前任务可能为网关中的任务 + if (targetIds.size() == 1) { + // 1 对 1 或 多 对 1 情况,currentIds 当前要跳转的节点列表(1或多),targetIds.get(0) 跳转到的节点(1) + // 如果驳回后的目标任务包含指定人,则直接通过变量回抄,如果没有则自动忽略该变量,不会给流程带来任何影响。 + ChangeActivityStateBuilder builder = runtimeService.createChangeActivityStateBuilder() + .processInstanceId(task.getProcessInstanceId()) + .moveActivityIdsToSingleActivityId(currentIds, targetIds.get(0)); + FlowTaskComment taskComment = + flowTaskCommentService.getLatestFlowTaskComment(task.getProcessInstanceId(), targetIds.get(0)); + String submitLoginName = taskComment.getCreateLoginName(); + if (StrUtil.isNotBlank(submitLoginName)) { + builder.localVariable(targetIds.get(0), FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR, submitLoginName); + } + builder.changeState(); + } + } + FlowTaskComment comment = new FlowTaskComment(); + comment.setTaskId(task.getId()); + comment.setTaskKey(task.getTaskDefinitionKey()); + comment.setTaskName(task.getName()); + comment.setApprovalType(forReject ? FlowApprovalType.REJECT : FlowApprovalType.REVOKE); + comment.setProcessInstanceId(task.getProcessInstanceId()); + comment.setTaskComment(reason); + flowTaskCommentService.saveNew(comment); + } catch (Exception e) { + log.error("Failed to execute moveSingleActivityIdToActivityIds", e); + return CallResult.error(e.getMessage()); + } + return CallResult.ok(); + } + + private List getParentUserTaskList( + FlowElement source, Set hasSequenceFlow, List userTaskList) { + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = getParentUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList); + } + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 类型为用户节点,则新增父级节点 + if (sequenceFlow.getSourceFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement()); + continue; + } + // 类型为子流程,则添加子流程开始节点出口处相连的节点 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + // 获取子流程用户任务节点 + List childUserTaskList = findChildProcessUserTasks( + (StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && !childUserTaskList.isEmpty()) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 网关场景的继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + userTaskList = getParentUserTaskList( + sequenceFlow.getSourceFlowElement(), new HashSet<>(hasSequenceFlow), userTaskList); + } + } + return userTaskList; + } + + private List getChildUserTaskList( + FlowElement source, List runActiveIdList, Set hasSequenceFlow, List flowElementList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + flowElementList = flowElementList == null ? new ArrayList<>() : flowElementList; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof EndEvent && source.getSubProcess() != null) { + flowElementList = getChildUserTaskList( + source.getSubProcess(), runActiveIdList, hasSequenceFlow, flowElementList); + } + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,或者为网关 + // 活动节点ID 在运行的任务中存在,添加 + FlowElement targetElement = sequenceFlow.getTargetFlowElement(); + if ((targetElement instanceof UserTask || targetElement instanceof Gateway) + && runActiveIdList.contains(targetElement.getId())) { + flowElementList.add(sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = getChildUserTaskList( + (FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), runActiveIdList, hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && !childUserTaskList.isEmpty()) { + flowElementList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + flowElementList = getChildUserTaskList( + sequenceFlow.getTargetFlowElement(), runActiveIdList, new HashSet<>(hasSequenceFlow), flowElementList); + } + } + return flowElementList; + } + + private List cleanHistoricTaskInstance( + Collection allElements, List historicActivityList) { + // 会签节点收集 + List multiTask = new ArrayList<>(); + allElements.forEach(flowElement -> { + if (flowElement instanceof UserTask) { + // 如果该节点的行为为会签行为,说明该节点为会签节点 + if (((UserTask) flowElement).getBehavior() instanceof ParallelMultiInstanceBehavior + || ((UserTask) flowElement).getBehavior() instanceof SequentialMultiInstanceBehavior) { + multiTask.add(flowElement.getId()); + } + } + }); + // 循环放入栈,栈 LIFO:后进先出 + Stack stack = new Stack<>(); + historicActivityList.forEach(stack::push); + // 清洗后的历史任务实例 + List lastHistoricTaskInstanceList = new ArrayList<>(); + // 网关存在可能只走了部分分支情况,且还存在跳转废弃数据以及其他分支数据的干扰,因此需要对历史节点数据进行清洗 + // 临时用户任务 key + StringBuilder userTaskKey = null; + // 临时被删掉的任务 key,存在并行情况 + List deleteKeyList = new ArrayList<>(); + // 临时脏数据线路 + List> dirtyDataLineList = new ArrayList<>(); + // 由某个点跳到会签点,此时出现多个会签实例对应 1 个跳转情况,需要把这些连续脏数据都找到 + // 会签特殊处理下标 + int multiIndex = -1; + // 会签特殊处理 key + StringBuilder multiKey = null; + // 会签特殊处理操作标识 + boolean multiOpera = false; + while (!stack.empty()) { + // 从这里开始 userTaskKey 都还是上个栈的 key + // 是否是脏数据线路上的点 + final boolean[] isDirtyData = {false}; + for (Set oldDirtyDataLine : dirtyDataLineList) { + if (oldDirtyDataLine.contains(stack.peek().getActivityId())) { + isDirtyData[0] = true; + } + } + // 删除原因不为空,说明从这条数据开始回跳或者回退的 + // MI_END:会签完成后,其他未签到节点的删除原因,不在处理范围内 + if (stack.peek().getDeleteReason() != null && !"MI_END".equals(stack.peek().getDeleteReason())) { + // 可以理解为脏线路起点 + String dirtyPoint = ""; + if (stack.peek().getDeleteReason().contains("Change activity to ")) { + dirtyPoint = stack.peek().getDeleteReason().replace("Change activity to ", ""); + } + // 会签回退删除原因有点不同 + if (stack.peek().getDeleteReason().contains("Change parent activity to ")) { + dirtyPoint = stack.peek().getDeleteReason().replace("Change parent activity to ", ""); + } + FlowElement dirtyTask = null; + // 获取变更节点的对应的入口处连线 + // 如果是网关并行回退情况,会变成两条脏数据路线,效果一样 + for (FlowElement flowElement : allElements) { + if (flowElement.getId().equals(stack.peek().getActivityId())) { + dirtyTask = flowElement; + } + } + // 获取脏数据线路 + Set dirtyDataLine = + findDirtyRoads(dirtyTask, null, null, StrUtil.split(dirtyPoint, ','), null); + // 自己本身也是脏线路上的点,加进去 + dirtyDataLine.add(stack.peek().getActivityId()); + log.info(stack.peek().getActivityId() + "点脏路线集合:" + dirtyDataLine); + // 是全新的需要添加的脏线路 + boolean isNewDirtyData = true; + for (Set strings : dirtyDataLineList) { + // 如果发现他的上个节点在脏线路内,说明这个点可能是并行的节点,或者连续驳回 + // 这时,都以之前的脏线路节点为标准,只需合并脏线路即可,也就是路线补全 + if (strings.contains(userTaskKey.toString())) { + isNewDirtyData = false; + strings.addAll(dirtyDataLine); + } + } + // 已确定时全新的脏线路 + if (isNewDirtyData) { + // deleteKey 单一路线驳回到并行,这种同时生成多个新实例记录情况,这时 deleteKey 其实是由多个值组成 + // 按照逻辑,回退后立刻生成的实例记录就是回退的记录 + // 至于驳回所生成的 Key,直接从删除原因中获取,因为存在驳回到并行的情况 + deleteKeyList.add(dirtyPoint + ","); + dirtyDataLineList.add(dirtyDataLine); + } + // 添加后,现在这个点变成脏线路上的点了 + isDirtyData[0] = true; + } + // 如果不是脏线路上的点,说明是有效数据,添加历史实例 Key + if (!isDirtyData[0]) { + lastHistoricTaskInstanceList.add(stack.peek().getActivityId()); + } + // 校验脏线路是否结束 + for (int i = 0; i < deleteKeyList.size(); i ++) { + // 如果发现脏数据属于会签,记录下下标与对应 Key,以备后续比对,会签脏数据范畴开始 + if (multiKey == null && multiTask.contains(stack.peek().getActivityId()) + && deleteKeyList.get(i).contains(stack.peek().getActivityId())) { + multiIndex = i; + multiKey = new StringBuilder(stack.peek().getActivityId()); + } + // 会签脏数据处理,节点退回会签清空 + // 如果在会签脏数据范畴中发现 Key改变,说明会签脏数据在上个节点就结束了,可以把会签脏数据删掉 + if (multiKey != null && !multiKey.toString().equals(stack.peek().getActivityId())) { + deleteKeyList.set(multiIndex , deleteKeyList.get(multiIndex).replace(stack.peek().getActivityId() + ",", "")); + multiKey = null; + // 结束进行下校验删除 + multiOpera = true; + } + // 其他脏数据处理 + // 发现该路线最后一条脏数据,说明这条脏数据线路处理完了,删除脏数据信息 + // 脏数据产生的新实例中是否包含这条数据 + if (multiKey == null && deleteKeyList.get(i).contains(stack.peek().getActivityId())) { + // 删除匹配到的部分 + deleteKeyList.set(i , deleteKeyList.get(i).replace(stack.peek().getActivityId() + ",", "")); + } + // 如果每组中的元素都以匹配过,说明脏数据结束 + if ("".equals(deleteKeyList.get(i))) { + // 同时删除脏数据 + deleteKeyList.remove(i); + dirtyDataLineList.remove(i); + break; + } + } + // 会签数据处理需要在循环外处理,否则可能导致溢出 + // 会签的数据肯定是之前放进去的所以理论上不会溢出,但还是校验下 + if (multiOpera && deleteKeyList.size() > multiIndex && "".equals(deleteKeyList.get(multiIndex))) { + // 同时删除脏数据 + deleteKeyList.remove(multiIndex); + dirtyDataLineList.remove(multiIndex); + multiIndex = -1; + multiOpera = false; + } + // pop() 方法与 peek() 方法不同,在返回值的同时,会把值从栈中移除 + // 保存新的 userTaskKey 在下个循环中使用 + userTaskKey = new StringBuilder(stack.pop().getActivityId()); + } + log.info("清洗后的历史节点数据:" + lastHistoricTaskInstanceList); + return lastHistoricTaskInstanceList; + } + + private List findChildProcessUserTasks(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = findChildProcessUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (childUserTaskList != null && !childUserTaskList.isEmpty()) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + userTaskList = findChildProcessUserTasks(sequenceFlow.getTargetFlowElement(), new HashSet<>(hasSequenceFlow), userTaskList); + } + } + return userTaskList; + } + + private Set findDirtyRoads( + FlowElement source, List passRoads, Set hasSequenceFlow, List targets, Set dirtyRoads) { + passRoads = passRoads == null ? new ArrayList<>() : passRoads; + dirtyRoads = dirtyRoads == null ? new HashSet<>() : dirtyRoads; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + dirtyRoads = findDirtyRoads(source.getSubProcess(), passRoads, hasSequenceFlow, targets, dirtyRoads); + } + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 新增经过的路线 + passRoads.add(sequenceFlow.getSourceFlowElement().getId()); + // 如果此点为目标点,确定经过的路线为脏线路,添加点到脏线路中,然后找下个连线 + if (targets.contains(sequenceFlow.getSourceFlowElement().getId())) { + dirtyRoads.addAll(passRoads); + continue; + } + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + dirtyRoads = findChildProcessAllDirtyRoad( + (StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, dirtyRoads); + // 是否存在子流程上,true 是,false 否 + Boolean isInChildProcess = dirtyTargetInChildProcess( + (StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, targets, null); + if (isInChildProcess) { + // 已在子流程上找到,该路线结束 + continue; + } + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + dirtyRoads = findDirtyRoads(sequenceFlow.getSourceFlowElement(), + new ArrayList<>(passRoads), new HashSet<>(hasSequenceFlow), targets, dirtyRoads); + } + } + return dirtyRoads; + } + + private Set findChildProcessAllDirtyRoad( + FlowElement source, Set hasSequenceFlow, Set dirtyRoads) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + dirtyRoads = dirtyRoads == null ? new HashSet<>() : dirtyRoads; + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 添加脏路线 + dirtyRoads.add(sequenceFlow.getTargetFlowElement().getId()); + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + dirtyRoads = findChildProcessAllDirtyRoad( + (FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, dirtyRoads); + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + dirtyRoads = findChildProcessAllDirtyRoad( + sequenceFlow.getTargetFlowElement(), new HashSet<>(hasSequenceFlow), dirtyRoads); + } + } + return dirtyRoads; + } + + private Boolean dirtyTargetInChildProcess( + FlowElement source, Set hasSequenceFlow, List targets, Boolean inChildProcess) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + inChildProcess = inChildProcess != null && inChildProcess; + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows != null && !inChildProcess) { + // 循环找到目标元素 + for (SequenceFlow sequenceFlow: sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果发现目标点在子流程上存在,说明只到子流程为止 + if (targets.contains(sequenceFlow.getTargetFlowElement().getId())) { + inChildProcess = true; + break; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + inChildProcess = dirtyTargetInChildProcess((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, targets, inChildProcess); + } + // 继续迭代 + // 注意:已经经过的节点与连线都应该用浅拷贝出来的对象 + // 比如分支:a->b->c与a->d->c,走完a->b->c后走另一个路线是,已经经过的节点应该不包含a->b->c路线的数据 + inChildProcess = dirtyTargetInChildProcess(sequenceFlow.getTargetFlowElement(), new HashSet<>(hasSequenceFlow), targets, inChildProcess); + } + } + return inChildProcess; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void transferTo(Task task, FlowTaskComment flowTaskComment) { + List transferUserList = StrUtil.split(flowTaskComment.getDelegateAssignee(), ","); + for (String transferUser : transferUserList) { + if (transferUser.equals(FlowConstant.START_USER_NAME_VAR)) { + String startUser = this.getProcessInstanceVariable( + task.getProcessInstanceId(), FlowConstant.PROC_INSTANCE_START_USER_NAME_VAR).toString(); + String newDelegateAssignee = StrUtil.replace( + flowTaskComment.getDelegateAssignee(), FlowConstant.START_USER_NAME_VAR, startUser); + flowTaskComment.setDelegateAssignee(newDelegateAssignee); + transferUserList = StrUtil.split(flowTaskComment.getDelegateAssignee(), ","); + break; + } + } + taskService.unclaim(task.getId()); + FlowTaskExt taskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId( + task.getProcessDefinitionId(), task.getTaskDefinitionKey()); + if (StrUtil.isNotBlank(taskExt.getCandidateUsernames())) { + List candidateUsernames = this.getCandidateUsernames(taskExt, task.getId()); + if (CollUtil.isNotEmpty(candidateUsernames)) { + for (String username : candidateUsernames) { + taskService.deleteCandidateUser(task.getId(), username); + } + } + } else if (StrUtil.equals(taskExt.getGroupType(), FlowConstant.GROUP_TYPE_ASSIGNEE)) { + List links = taskService.getIdentityLinksForTask(task.getId()); + for (IdentityLink link : links) { + taskService.deleteUserIdentityLink(task.getId(), link.getUserId(), link.getType()); + } + } else { + this.removeCandidateGroup(taskExt, task); + } + transferUserList.forEach(u -> taskService.addCandidateUser(task.getId(), u)); + flowTaskComment.fillWith(task); + flowTaskCommentService.saveNew(flowTaskComment); + } + + @Override + public List getCandidateUsernames(FlowTaskExt flowTaskExt, String taskId) { + if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { + return Collections.emptyList(); + } + if (!StrUtil.equals(flowTaskExt.getCandidateUsernames(), "${" + FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR + "}")) { + return StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); + } + Object candidateUsernames = getTaskVariableStringWithSafe(taskId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); + return candidateUsernames == null ? null : StrUtil.split(candidateUsernames.toString(), ","); + } + + @Override + public Tuple2, Set> getDeptPostIdAndPostIds( + FlowTaskExt flowTaskExt, String processInstanceId, boolean historic) { + Set postIdSet = new LinkedHashSet<>(); + Set deptPostIdSet = new LinkedHashSet<>(); + if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + Object v = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, historic); + if (ObjectUtil.isNotEmpty(v)) { + deptPostIdSet.add(v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + Object v = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, historic); + if (ObjectUtil.isNotEmpty(v)) { + deptPostIdSet.add(v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_POST) + && StrUtil.isNotBlank(flowTaskExt.getDeptPostListJson())) { + this.buildDeptPostIdAndPostIdsForPost(flowTaskExt, processInstanceId, historic, postIdSet, deptPostIdSet); + } + return new Tuple2<>(deptPostIdSet, postIdSet); + } + + @Override + public Map getAllUserTaskMap(String processDefinitionId) { + BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId); + Process process = bpmnModel.getProcesses().get(0); + return process.findFlowElementsOfType(UserTask.class) + .stream().collect(Collectors.toMap(UserTask::getId, a -> a, (k1, k2) -> k1)); + } + + @Override + public UserTask getUserTask(String processDefinitionId, String taskKey) { + BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId); + for (Process process : bpmnModel.getProcesses()) { + UserTask userTask = process.findFlowElementsOfType(UserTask.class) + .stream().filter(t -> t.getId().equals(taskKey)).findFirst().orElse(null); + if (userTask != null) { + return userTask; + } + } + return null; + } + + private void doChangeState(String processInstanceId, List currentIds, List targetIds) { + if (ObjectUtil.hasEmpty(currentIds, targetIds)) { + throw new MyRuntimeException("跳转的源节点和任务节点数量均不能为空!"); + } + ChangeActivityStateBuilder builder = + this.createChangeActivityStateBuilder(currentIds, targetIds, processInstanceId); + targetIds.forEach(targetId -> { + FlowTaskComment comment = flowTaskCommentService.getLatestFlowTaskComment(processInstanceId, targetId); + if (comment != null && StrUtil.isNotBlank(comment.getCreateLoginName())) { + builder.localVariable(targetId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR, comment.getCreateLoginName()); + } + }); + builder.changeState(); + } + + private ChangeActivityStateBuilder createChangeActivityStateBuilder( + List currentIds, List targetIds, String processInstanceId) { + ChangeActivityStateBuilder builder; + if (currentIds.size() > 1 && targetIds.size() > 1) { + builder = new CustomChangeActivityStateBuilderImpl((RuntimeServiceImpl) runtimeService); + ((CustomChangeActivityStateBuilderImpl) builder) + .moveActivityIdsToActivityIds(currentIds, targetIds) + .processInstanceId(processInstanceId); + } else { + builder = runtimeService.createChangeActivityStateBuilder().processInstanceId(processInstanceId); + if (targetIds.size() == 1) { + if (currentIds.size() == 1) { + builder.moveActivityIdTo(currentIds.get(0), targetIds.get(0)); + } else { + builder.moveActivityIdsToSingleActivityId(currentIds, targetIds.get(0)); + } + } else { + builder.moveSingleActivityIdToActivityIds(currentIds.get(0), targetIds); + } + } + return builder; + } + + private void removeCandidateGroup(FlowTaskExt taskExt, Task task) { + if (StrUtil.isNotBlank(taskExt.getDeptIds())) { + for (String deptId : StrUtil.split(taskExt.getDeptIds(), ",")) { + taskService.deleteCandidateGroup(task.getId(), deptId); + } + } + if (StrUtil.isNotBlank(taskExt.getRoleIds())) { + for (String roleId : StrUtil.split(taskExt.getRoleIds(), ",")) { + taskService.deleteCandidateGroup(task.getId(), roleId); + } + } + Tuple2, Set> tuple2 = + getDeptPostIdAndPostIds(taskExt, task.getProcessInstanceId(), false); + if (CollUtil.isNotEmpty(tuple2.getFirst())) { + for (String deptPostId : tuple2.getFirst()) { + taskService.deleteCandidateGroup(task.getId(), deptPostId); + } + } + if (CollUtil.isNotEmpty(tuple2.getSecond())) { + for (String postId : tuple2.getSecond()) { + taskService.deleteCandidateGroup(task.getId(), postId); + } + } + } + + private void buildDeptPostIdAndPostIdsForPost( + FlowTaskExt flowTaskExt, + String processInstanceId, + boolean historic, + Set postIdSet, + Set deptPostIdSet) { + List groupDataList = + JSON.parseArray(flowTaskExt.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR: + postIdSet.add(groupData.getPostId()); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_VAR: + deptPostIdSet.add(groupData.getDeptPostId()); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + Object v = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.SELF_DEPT_POST_PREFIX + groupData.getPostId(), historic); + if (ObjectUtil.isNotEmpty(v)) { + deptPostIdSet.add(v.toString()); + } + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + Object v2 = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.UP_DEPT_POST_PREFIX + groupData.getPostId(), historic); + if (ObjectUtil.isNotEmpty(v2)) { + deptPostIdSet.add(v2.toString()); + } + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + Object v3 = this.getProcessInstanceVariable( + processInstanceId, FlowConstant.SIBLING_DEPT_POST_PREFIX + groupData.getPostId(), historic); + if (ObjectUtil.isNotEmpty(v3)) { + deptPostIdSet.addAll(StrUtil.split(v3.toString(), ",") + .stream().filter(StrUtil::isNotBlank).toList()); + } + break; + default: + break; + } + } + } + + private Object getProcessInstanceVariable(String processInstanceId, String variableName, boolean historic) { + if (historic) { + return getHistoricProcessInstanceVariable(processInstanceId, variableName); + } + return getProcessInstanceVariable(processInstanceId, variableName); + } + + private void handleMultiInstanceApprovalType(String executionId, String approvalType, JSONObject taskVariableData) { + if (StrUtil.isBlank(approvalType)) { + return; + } + if (StrUtil.equalsAny(approvalType, + FlowApprovalType.MULTI_AGREE, + FlowApprovalType.MULTI_REFUSE, + FlowApprovalType.MULTI_ABSTAIN)) { + Map variables = runtimeService.getVariables(executionId); + Integer agreeCount = (Integer) variables.get(FlowConstant.MULTI_AGREE_COUNT_VAR); + Integer refuseCount = (Integer) variables.get(FlowConstant.MULTI_REFUSE_COUNT_VAR); + Integer abstainCount = (Integer) variables.get(FlowConstant.MULTI_ABSTAIN_COUNT_VAR); + Integer nrOfInstances = (Integer) variables.get(FlowConstant.NUMBER_OF_INSTANCES_VAR); + taskVariableData.put(FlowConstant.MULTI_AGREE_COUNT_VAR, agreeCount); + taskVariableData.put(FlowConstant.MULTI_REFUSE_COUNT_VAR, refuseCount); + taskVariableData.put(FlowConstant.MULTI_ABSTAIN_COUNT_VAR, abstainCount); + taskVariableData.put(FlowConstant.MULTI_SIGN_NUM_OF_INSTANCES_VAR, nrOfInstances); + switch (approvalType) { + case FlowApprovalType.MULTI_AGREE: + if (agreeCount == null) { + agreeCount = 0; + } + taskVariableData.put(FlowConstant.MULTI_AGREE_COUNT_VAR, agreeCount + 1); + break; + case FlowApprovalType.MULTI_REFUSE: + if (refuseCount == null) { + refuseCount = 0; + } + taskVariableData.put(FlowConstant.MULTI_REFUSE_COUNT_VAR, refuseCount + 1); + break; + case FlowApprovalType.MULTI_ABSTAIN: + if (abstainCount == null) { + abstainCount = 0; + } + taskVariableData.put(FlowConstant.MULTI_ABSTAIN_COUNT_VAR, abstainCount + 1); + break; + default: + break; + } + } + } + + private TaskQuery createQuery() { + TaskQuery query = taskService.createTaskQuery().active(); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() != null) { + query.taskTenantId(tokenData.getTenantId().toString()); + } else { + if (StrUtil.isBlank(tokenData.getAppCode())) { + query.taskWithoutTenantId(); + } else { + query.taskTenantId(tokenData.getAppCode()); + } + } + return query; + } + + private void buildCandidateCondition(TaskQuery query, String loginName) { + Set groupIdSet = new HashSet<>(); + // NOTE: 需要注意的是,部门Id、部门岗位Id,或者其他类型的分组Id,他们之间一定不能重复。 + TokenData tokenData = TokenData.takeFromRequest(); + Object deptId = tokenData.getDeptId(); + if (deptId != null) { + groupIdSet.add(deptId.toString()); + } + String roleIds = tokenData.getRoleIds(); + if (StrUtil.isNotBlank(tokenData.getRoleIds())) { + groupIdSet.addAll(StrUtil.split(roleIds, ",")); + } + String postIds = tokenData.getPostIds(); + if (StrUtil.isNotBlank(tokenData.getPostIds())) { + groupIdSet.addAll(StrUtil.split(postIds, ",")); + } + String deptPostIds = tokenData.getDeptPostIds(); + if (StrUtil.isNotBlank(deptPostIds)) { + groupIdSet.addAll(StrUtil.split(deptPostIds, ",")); + } + if (CollUtil.isNotEmpty(groupIdSet)) { + query.or().taskCandidateGroupIn(groupIdSet).taskCandidateOrAssigned(loginName).endOr(); + } else { + query.taskCandidateOrAssigned(loginName); + } + } + + private String buildMutiSignAssigneeList(String operationListJson) { + FlowTaskMultiSignAssign multiSignAssignee = null; + List taskOperationList = JSONArray.parseArray(operationListJson, FlowTaskOperation.class); + for (FlowTaskOperation taskOperation : taskOperationList) { + if (FlowApprovalType.MULTI_SIGN.equals(taskOperation.getType())) { + multiSignAssignee = taskOperation.getMultiSignAssignee(); + break; + } + } + org.springframework.util.Assert.notNull(multiSignAssignee, "multiSignAssignee can't be NULL"); + if (UserFilterGroup.USER.equals(multiSignAssignee.getAssigneeType())) { + return multiSignAssignee.getAssigneeList(); + } + Set usernameSet = null; + BaseFlowIdentityExtHelper extHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + Set idSet = CollUtil.newHashSet(StrUtil.split(multiSignAssignee.getAssigneeList(), ",")); + switch (multiSignAssignee.getAssigneeType()) { + case UserFilterGroup.ROLE -> usernameSet = extHelper.getUsernameListByRoleIds(idSet); + case UserFilterGroup.DEPT -> usernameSet = extHelper.getUsernameListByDeptIds(idSet); + case UserFilterGroup.POST -> usernameSet = extHelper.getUsernameListByPostIds(idSet); + case UserFilterGroup.DEPT_POST -> usernameSet = extHelper.getUsernameListByDeptPostIds(idSet); + default -> { + } + } + return CollUtil.isEmpty(usernameSet) ? null : CollUtil.join(usernameSet, ","); + } + + private Collection getAllElements(Collection flowElements, Collection allElements) { + allElements = allElements == null ? new ArrayList<>() : allElements; + for (FlowElement flowElement : flowElements) { + allElements.add(flowElement); + if (flowElement instanceof SubProcess) { + allElements = getAllElements(((SubProcess) flowElement).getFlowElements(), allElements); + } + } + return allElements; + } + + private void doChangeTask(Task runtimeTask) { + Map allUserTaskMap = + this.getAllUserTaskMap(runtimeTask.getProcessDefinitionId()); + UserTask userTaskModel = allUserTaskMap.get(runtimeTask.getTaskDefinitionKey()); + String completeCondition = userTaskModel.getLoopCharacteristics().getCompletionCondition(); + Execution parentExecution = this.getMultiInstanceRootExecution(runtimeTask); + Object nrOfCompletedInstances = runtimeService.getVariable( + parentExecution.getId(), FlowConstant.NUMBER_OF_COMPLETED_INSTANCES_VAR); + Object nrOfInstances = runtimeService.getVariable( + parentExecution.getId(), FlowConstant.NUMBER_OF_INSTANCES_VAR); + ExpressionFactory factory = new ExpressionFactoryImpl(); + SimpleContext context = new SimpleContext(); + context.setVariable("nrOfCompletedInstances", + factory.createValueExpression(nrOfCompletedInstances, Integer.class)); + context.setVariable("nrOfInstances", + factory.createValueExpression(nrOfInstances, Integer.class)); + ValueExpression e = factory.createValueExpression(context, completeCondition, Boolean.class); + Boolean ok = Convert.convert(Boolean.class, e.getValue(context)); + if (BooleanUtil.isTrue(ok)) { + FlowElement targetKey = userTaskModel.getOutgoingFlows().get(0).getTargetFlowElement(); + ChangeActivityStateBuilder builder = runtimeService.createChangeActivityStateBuilder() + .processInstanceId(runtimeTask.getProcessInstanceId()) + .moveActivityIdTo(userTaskModel.getId(), targetKey.getId()); + builder.localVariable(targetKey.getId(), FlowConstant.MULTI_SIGN_NUM_OF_INSTANCES_VAR, nrOfInstances); + builder.changeState(); + } + } + + private Execution getMultiInstanceRootExecution(Task runtimeTask) { + List executionList = runtimeService.createExecutionQuery() + .processInstanceId(runtimeTask.getProcessInstanceId()) + .activityId(runtimeTask.getTaskDefinitionKey()).list(); + for (Execution e : executionList) { + ExecutionEntityImpl ee = (ExecutionEntityImpl) e; + if (ee.isMultiInstanceRoot()) { + return e; + } + } + Execution execution = executionList.get(0); + return runtimeService.createExecutionQuery() + .processInstanceId(runtimeTask.getProcessInstanceId()) + .executionId(execution.getParentId()).singleResult(); + } + + private List getElementIncomingFlows(FlowElement source) { + List sequenceFlows = null; + if (source instanceof org.flowable.bpmn.model.Task) { + sequenceFlows = ((org.flowable.bpmn.model.Task) source).getIncomingFlows(); + } else if (source instanceof Gateway) { + sequenceFlows = ((Gateway) source).getIncomingFlows(); + } else if (source instanceof SubProcess) { + sequenceFlows = ((SubProcess) source).getIncomingFlows(); + } else if (source instanceof StartEvent) { + sequenceFlows = ((StartEvent) source).getIncomingFlows(); + } else if (source instanceof EndEvent) { + sequenceFlows = ((EndEvent) source).getIncomingFlows(); + } + return sequenceFlows; + } + + private List getElementOutgoingFlows(FlowElement source) { + List sequenceFlows = null; + if (source instanceof org.flowable.bpmn.model.Task) { + sequenceFlows = ((org.flowable.bpmn.model.Task) source).getOutgoingFlows(); + } else if (source instanceof Gateway) { + sequenceFlows = ((Gateway) source).getOutgoingFlows(); + } else if (source instanceof SubProcess) { + sequenceFlows = ((SubProcess) source).getOutgoingFlows(); + } else if (source instanceof StartEvent) { + sequenceFlows = ((StartEvent) source).getOutgoingFlows(); + } else if (source instanceof EndEvent) { + sequenceFlows = ((EndEvent) source).getOutgoingFlows(); + } + return sequenceFlows; + } + + private FlowableListener createListener(String eventName, String listenerClassName) { + FlowableListener listener = new FlowableListener(); + listener.setEvent(eventName); + listener.setImplementationType("class"); + listener.setImplementation(listenerClassName); + return listener; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java new file mode 100644 index 00000000..de1fb85a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowCategoryServiceImpl.java @@ -0,0 +1,131 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.github.pagehelper.Page; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.service.*; +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.Date; +import java.util.List; +import java.util.Set; +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowCategoryService") +public class FlowCategoryServiceImpl extends BaseService implements FlowCategoryService { + + @Autowired + private FlowCategoryMapper flowCategoryMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowCategoryMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowCategory saveNew(FlowCategory flowCategory) { + flowCategory.setCategoryId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + flowCategory.setAppCode(tokenData.getAppCode()); + flowCategory.setTenantId(tokenData.getTenantId()); + flowCategory.setUpdateUserId(tokenData.getUserId()); + flowCategory.setCreateUserId(tokenData.getUserId()); + Date now = new Date(); + flowCategory.setUpdateTime(now); + flowCategory.setCreateTime(now); + flowCategoryMapper.insert(flowCategory); + return flowCategory; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(FlowCategory flowCategory, FlowCategory originalFlowCategory) { + TokenData tokenData = TokenData.takeFromRequest(); + flowCategory.setAppCode(tokenData.getAppCode()); + flowCategory.setTenantId(tokenData.getTenantId()); + flowCategory.setUpdateUserId(tokenData.getUserId()); + flowCategory.setCreateUserId(originalFlowCategory.getCreateUserId()); + flowCategory.setUpdateTime(new Date()); + flowCategory.setCreateTime(originalFlowCategory.getCreateTime()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = + this.createUpdateQueryForNullValue(flowCategory, flowCategory.getCategoryId()); + return flowCategoryMapper.update(flowCategory, uw) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long categoryId) { + return flowCategoryMapper.deleteById(categoryId) == 1; + } + + @Override + public List getFlowCategoryList(FlowCategory filter, String orderBy) { + if (filter == null) { + filter = new FlowCategory(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return flowCategoryMapper.getFlowCategoryList(filter, orderBy); + } + + @Override + public List getFlowCategoryListWithRelation(FlowCategory filter, String orderBy) { + List resultList = this.getFlowCategoryList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public boolean existByCode(String code) { + FlowCategory filter = new FlowCategory(); + filter.setCode(code); + return CollUtil.isNotEmpty(this.getFlowCategoryList(filter, null)); + } + + @Override + public List getInList(Set categoryIds) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.in(FlowCategory::getCategoryId, categoryIds); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getAppCode() == null) { + qw.isNull(FlowCategory::getAppCode); + } else { + qw.eq(FlowCategory::getAppCode, tokenData.getAppCode()); + } + if (tokenData.getTenantId() == null) { + qw.isNull(FlowCategory::getTenantId); + } else { + qw.eq(FlowCategory::getTenantId, tokenData.getTenantId()); + } + return flowCategoryMapper.selectList(qw); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java new file mode 100644 index 00000000..adb7dca0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryServiceImpl.java @@ -0,0 +1,490 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.dao.FlowEntryMapper; +import com.orangeforms.common.flow.dao.FlowEntryPublishMapper; +import com.orangeforms.common.flow.dao.FlowEntryPublishVariableMapper; +import com.orangeforms.common.flow.listener.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.model.constant.FlowVariableType; +import com.orangeforms.common.flow.object.FlowElementExtProperty; +import com.orangeforms.common.flow.object.FlowEntryExtensionData; +import com.orangeforms.common.flow.object.FlowTaskPostCandidateGroup; +import com.orangeforms.common.flow.object.FlowUserTaskExtData; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.util.FlowRedisKeyUtil; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.*; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowEntryService") +public class FlowEntryServiceImpl extends BaseService implements FlowEntryService { + + @Autowired + private FlowEntryMapper flowEntryMapper; + @Autowired + private FlowEntryPublishMapper flowEntryPublishMapper; + @Autowired + private FlowEntryPublishVariableMapper flowEntryPublishVariableMapper; + @Autowired + private FlowEntryVariableService flowEntryVariableService; + @Autowired + private FlowCategoryService flowCategoryService; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private RepositoryService repositoryService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + + private static final Integer FLOW_ENTRY_PUBLISH_TTL = 60 * 60 * 24; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowEntryMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowEntry saveNew(FlowEntry flowEntry) { + flowEntry.setEntryId(idGenerator.nextLongId()); + flowEntry.setStatus(FlowEntryStatus.UNPUBLISHED); + TokenData tokenData = TokenData.takeFromRequest(); + flowEntry.setAppCode(tokenData.getAppCode()); + flowEntry.setTenantId(tokenData.getTenantId()); + flowEntry.setUpdateUserId(tokenData.getUserId()); + flowEntry.setCreateUserId(tokenData.getUserId()); + Date now = new Date(); + flowEntry.setUpdateTime(now); + flowEntry.setCreateTime(now); + flowEntryMapper.insert(flowEntry); + this.insertBuiltinEntryVariables(flowEntry.getEntryId()); + return flowEntry; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void publish(FlowEntry flowEntry, String initTaskInfo) throws XMLStreamException { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + FlowCategory flowCategory = flowCategoryService.getById(flowEntry.getCategoryId()); + InputStream xmlStream = new ByteArrayInputStream( + flowEntry.getBpmnXml().getBytes(StandardCharsets.UTF_8)); + @Cleanup XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(xmlStream); + BpmnXMLConverter converter = new BpmnXMLConverter(); + BpmnModel bpmnModel = converter.convertToBpmnModel(reader); + bpmnModel.getMainProcess().setName(flowEntry.getProcessDefinitionName()); + bpmnModel.getMainProcess().setId(flowEntry.getProcessDefinitionKey()); + flowApiService.addProcessInstanceEndListener(bpmnModel, FlowFinishedListener.class); + List flowTaskExtList = flowTaskExtService.buildTaskExtList(bpmnModel); + if (StrUtil.isNotBlank(flowEntry.getExtensionData())) { + FlowEntryExtensionData flowEntryExtensionData = + JSON.parseObject(flowEntry.getExtensionData(), FlowEntryExtensionData.class); + this.mergeTaskNotifyData(flowEntryExtensionData, flowTaskExtList); + } + this.processFlowTaskExtList(flowTaskExtList, bpmnModel); + TokenData tokenData = TokenData.takeFromRequest(); + Deployment deploy = repositoryService.createDeployment() + .addBpmnModel(flowEntry.getProcessDefinitionKey() + ".bpmn", bpmnModel) + .tenantId(tokenData.getTenantId() != null ? tokenData.getTenantId().toString() : tokenData.getAppCode()) + .name(flowEntry.getProcessDefinitionName()) + .key(flowEntry.getProcessDefinitionKey()) + .category(flowCategory.getCode()) + .deploy(); + ProcessDefinition processDefinition = flowApiService.getProcessDefinitionByDeployId(deploy.getId()); + FlowEntryPublish flowEntryPublish = new FlowEntryPublish(); + flowEntryPublish.setEntryPublishId(idGenerator.nextLongId()); + flowEntryPublish.setEntryId(flowEntry.getEntryId()); + flowEntryPublish.setProcessDefinitionId(processDefinition.getId()); + flowEntryPublish.setDeployId(processDefinition.getDeploymentId()); + flowEntryPublish.setPublishVersion(processDefinition.getVersion()); + flowEntryPublish.setActiveStatus(true); + flowEntryPublish.setMainVersion(flowEntry.getStatus().equals(FlowEntryStatus.UNPUBLISHED)); + flowEntryPublish.setCreateUserId(TokenData.takeFromRequest().getUserId()); + flowEntryPublish.setPublishTime(new Date()); + flowEntryPublish.setInitTaskInfo(initTaskInfo); + flowEntryPublish.setExtensionData(flowEntry.getExtensionData()); + flowEntryPublishMapper.insert(flowEntryPublish); + FlowEntry updatedFlowEntry = new FlowEntry(); + updatedFlowEntry.setEntryId(flowEntry.getEntryId()); + updatedFlowEntry.setStatus(FlowEntryStatus.PUBLISHED); + updatedFlowEntry.setLatestPublishTime(new Date()); + // 对于从未发布过的工作,第一次发布的时候会将本地发布置位主版本。 + if (flowEntry.getStatus().equals(FlowEntryStatus.UNPUBLISHED)) { + updatedFlowEntry.setMainEntryPublishId(flowEntryPublish.getEntryPublishId()); + } + flowEntryMapper.updateById(updatedFlowEntry); + FlowEntryVariable flowEntryVariableFilter = new FlowEntryVariable(); + flowEntryVariableFilter.setEntryId(flowEntry.getEntryId()); + List flowEntryVariableList = + flowEntryVariableService.getFlowEntryVariableList(flowEntryVariableFilter, null); + if (CollUtil.isNotEmpty(flowTaskExtList)) { + flowTaskExtList.forEach(t -> t.setProcessDefinitionId(processDefinition.getId())); + flowTaskExtService.saveBatch(flowTaskExtList); + } + this.insertEntryPublishVariables(flowEntryVariableList, flowEntryPublish.getEntryPublishId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(FlowEntry flowEntry, FlowEntry originalFlowEntry) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + TokenData tokenData = TokenData.takeFromRequest(); + flowEntry.setAppCode(tokenData.getAppCode()); + flowEntry.setTenantId(tokenData.getTenantId()); + flowEntry.setUpdateUserId(tokenData.getUserId()); + flowEntry.setCreateUserId(originalFlowEntry.getCreateUserId()); + flowEntry.setUpdateTime(new Date()); + flowEntry.setCreateTime(originalFlowEntry.getCreateTime()); + flowEntry.setPageId(originalFlowEntry.getPageId()); + return flowEntryMapper.updateById(flowEntry) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long entryId) { + FlowEntry flowEntry = this.getById(entryId); + if (flowEntry != null) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + } + if (flowEntryMapper.deleteById(entryId) != 1) { + return false; + } + flowEntryVariableService.removeByEntryId(entryId); + return true; + } + + @Override + public List getFlowEntryList(FlowEntry filter, String orderBy) { + if (filter == null) { + filter = new FlowEntry(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return flowEntryMapper.getFlowEntryList(filter, orderBy); + } + + @Override + public List getFlowEntryListWithRelation(FlowEntry filter, String orderBy) { + List resultList = this.getFlowEntryList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + Set mainEntryPublishIdSet = resultList.stream() + .map(FlowEntry::getMainEntryPublishId).filter(Objects::nonNull).collect(Collectors.toSet()); + if (CollUtil.isNotEmpty(mainEntryPublishIdSet)) { + List mainEntryPublishList = + flowEntryPublishMapper.selectBatchIds(mainEntryPublishIdSet); + MyModelUtil.makeOneToOneRelation(FlowEntry.class, resultList, FlowEntry::getMainEntryPublishId, + mainEntryPublishList, FlowEntryPublish::getEntryPublishId, "mainFlowEntryPublish"); + } + return resultList; + } + + @Override + public FlowEntry getFlowEntryFromCache(String processDefinitionKey) { + String key = FlowRedisKeyUtil.makeFlowEntryKey(processDefinitionKey); + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(FlowEntry::getProcessDefinitionKey, processDefinitionKey); + TokenData tokenData = TokenData.takeFromRequest(); + if (StrUtil.isNotBlank(tokenData.getAppCode())) { + qw.eq(FlowEntry::getAppCode, tokenData.getAppCode()); + } else { + qw.isNull(FlowEntry::getAppCode); + } + if (tokenData.getTenantId() != null) { + qw.eq(FlowEntry::getTenantId, tokenData.getTenantId()); + } else { + qw.isNull(FlowEntry::getTenantId); + } + return commonRedisUtil.getFromCacheWithQueryWrapper(key, qw, flowEntryMapper::selectOne, FlowEntry.class); + } + + @Override + public List getFlowEntryPublishList(Long entryId) { + FlowEntryPublish filter = new FlowEntryPublish(); + filter.setEntryId(entryId); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(filter); + queryWrapper.orderByDesc(FlowEntryPublish::getEntryPublishId); + return flowEntryPublishMapper.selectList(queryWrapper); + } + + @Override + public List getFlowEntryPublishList(Set processDefinitionIdSet) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(FlowEntryPublish::getProcessDefinitionId, processDefinitionIdSet); + return flowEntryPublishMapper.selectList(queryWrapper); + } + + @Override + public FlowEntryPublish getFlowEntryPublishFromCache(Long entryPublishId) { + String key = FlowRedisKeyUtil.makeFlowEntryPublishKey(entryPublishId); + return commonRedisUtil.getFromCache( + key, entryPublishId, flowEntryPublishMapper::selectById, FlowEntryPublish.class, FLOW_ENTRY_PUBLISH_TTL); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFlowEntryMainVersion(FlowEntry flowEntry, FlowEntryPublish newMainFlowEntryPublish) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryKey(flowEntry.getProcessDefinitionKey())); + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(newMainFlowEntryPublish.getEntryPublishId())); + FlowEntryPublish oldMainFlowEntryPublish = + flowEntryPublishMapper.selectById(flowEntry.getMainEntryPublishId()); + if (oldMainFlowEntryPublish != null) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(oldMainFlowEntryPublish.getEntryPublishId())); + oldMainFlowEntryPublish.setMainVersion(false); + flowEntryPublishMapper.updateById(oldMainFlowEntryPublish); + } + newMainFlowEntryPublish.setMainVersion(true); + flowEntryPublishMapper.updateById(newMainFlowEntryPublish); + FlowEntry updatedEntry = new FlowEntry(); + updatedEntry.setEntryId(flowEntry.getEntryId()); + updatedEntry.setMainEntryPublishId(newMainFlowEntryPublish.getEntryPublishId()); + flowEntryMapper.updateById(updatedEntry); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void suspendFlowEntryPublish(FlowEntryPublish flowEntryPublish) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(flowEntryPublish.getEntryPublishId())); + FlowEntryPublish updatedEntryPublish = new FlowEntryPublish(); + updatedEntryPublish.setEntryPublishId(flowEntryPublish.getEntryPublishId()); + updatedEntryPublish.setActiveStatus(false); + flowEntryPublishMapper.updateById(updatedEntryPublish); + flowApiService.suspendProcessDefinition(flowEntryPublish.getProcessDefinitionId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void activateFlowEntryPublish(FlowEntryPublish flowEntryPublish) { + commonRedisUtil.evictFormCache( + FlowRedisKeyUtil.makeFlowEntryPublishKey(flowEntryPublish.getEntryPublishId())); + FlowEntryPublish updatedEntryPublish = new FlowEntryPublish(); + updatedEntryPublish.setEntryPublishId(flowEntryPublish.getEntryPublishId()); + updatedEntryPublish.setActiveStatus(true); + flowEntryPublishMapper.updateById(updatedEntryPublish); + flowApiService.activateProcessDefinition(flowEntryPublish.getProcessDefinitionId()); + } + + @Override + public boolean existByProcessDefinitionKey(String processDefinitionKey) { + FlowEntry filter = new FlowEntry(); + filter.setProcessDefinitionKey(processDefinitionKey); + return CollUtil.isNotEmpty(this.getFlowEntryList(filter, null)); + } + + @Override + public CallResult verifyRelatedData(FlowEntry flowEntry, FlowEntry originalFlowEntry) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + if (this.needToVerify(flowEntry, originalFlowEntry, FlowEntry::getCategoryId) + && !flowCategoryService.existId(flowEntry.getCategoryId())) { + return CallResult.error(String.format(errorMessageFormat, "流程类别Id")); + } + return CallResult.ok(); + } + + private void insertBuiltinEntryVariables(Long entryId) { + Date now = new Date(); + FlowEntryVariable operationTypeVariable = new FlowEntryVariable(); + operationTypeVariable.setVariableId(idGenerator.nextLongId()); + operationTypeVariable.setEntryId(entryId); + operationTypeVariable.setVariableName(FlowConstant.OPERATION_TYPE_VAR); + operationTypeVariable.setShowName("审批类型"); + operationTypeVariable.setVariableType(FlowVariableType.TASK); + operationTypeVariable.setBuiltin(true); + operationTypeVariable.setCreateTime(now); + flowEntryVariableService.saveNew(operationTypeVariable); + FlowEntryVariable startUserNameVariable = new FlowEntryVariable(); + startUserNameVariable.setVariableId(idGenerator.nextLongId()); + startUserNameVariable.setEntryId(entryId); + startUserNameVariable.setVariableName("startUserName"); + startUserNameVariable.setShowName("流程启动用户"); + startUserNameVariable.setVariableType(FlowVariableType.INSTANCE); + startUserNameVariable.setBuiltin(true); + startUserNameVariable.setCreateTime(now); + flowEntryVariableService.saveNew(startUserNameVariable); + } + + private void insertEntryPublishVariables(List entryVariableList, Long entryPublishId) { + if (CollUtil.isEmpty(entryVariableList)) { + return; + } + List entryPublishVariableList = + MyModelUtil.copyCollectionTo(entryVariableList, FlowEntryPublishVariable.class); + for (FlowEntryPublishVariable variable : entryPublishVariableList) { + variable.setVariableId(idGenerator.nextLongId()); + variable.setEntryPublishId(entryPublishId); + } + flowEntryPublishVariableMapper.insertList(entryPublishVariableList); + } + + private void mergeTaskNotifyData(FlowEntryExtensionData flowEntryExtensionData, List flowTaskExtList) { + if (CollUtil.isEmpty(flowEntryExtensionData.getNotifyTypes())) { + return; + } + List flowTaskNotifyTypes = + flowEntryExtensionData.getNotifyTypes().stream().filter(StrUtil::isNotBlank).collect(Collectors.toList()); + if (CollUtil.isEmpty(flowTaskNotifyTypes)) { + return; + } + for (FlowTaskExt flowTaskExt : flowTaskExtList) { + if (flowTaskExt.getExtraDataJson() == null) { + JSONObject o = new JSONObject(); + o.put(FlowConstant.USER_TASK_NOTIFY_TYPES_KEY, flowTaskNotifyTypes); + flowTaskExt.setExtraDataJson(o.toJSONString()); + } else { + FlowUserTaskExtData taskExtData = + JSON.parseObject(flowTaskExt.getExtraDataJson(), FlowUserTaskExtData.class); + if (CollUtil.isEmpty(taskExtData.getFlowNotifyTypeList())) { + taskExtData.setFlowNotifyTypeList(flowTaskNotifyTypes); + } else { + Set notifyTypesSet = taskExtData.getFlowNotifyTypeList() + .stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + notifyTypesSet.addAll(flowTaskNotifyTypes); + taskExtData.setFlowNotifyTypeList(new LinkedList<>(notifyTypesSet)); + } + flowTaskExt.setExtraDataJson(JSON.toJSONString(taskExtData)); + } + } + } + + private void doAddLatestApprovalStatusListener(Collection elementList) { + List sequenceFlowList = + elementList.stream().filter(SequenceFlow.class::isInstance).toList(); + for (FlowElement sequenceFlow : sequenceFlowList) { + FlowElementExtProperty extProperty = flowTaskExtService.buildFlowElementExt(sequenceFlow); + if (extProperty != null && extProperty.getLatestApprovalStatus() != null) { + List fieldExtensions = new LinkedList<>(); + FieldExtension fieldExtension = new FieldExtension(); + fieldExtension.setFieldName(FlowConstant.LATEST_APPROVAL_STATUS_KEY); + fieldExtension.setStringValue(extProperty.getLatestApprovalStatus().toString()); + fieldExtensions.add(fieldExtension); + flowApiService.addExecutionListener( + sequenceFlow, UpdateLatestApprovalStatusListener.class, "start", fieldExtensions); + } + } + List subProcesseList = elementList.stream() + .filter(SubProcess.class::isInstance).map(SubProcess.class::cast).toList(); + for (SubProcess subProcess : subProcesseList) { + this.doAddLatestApprovalStatusListener(subProcess.getFlowElements()); + } + } + + private void calculateAllElementList(Collection elements, List resultList) { + resultList.addAll(elements); + for (FlowElement element : elements) { + if (element instanceof SubProcess) { + this.calculateAllElementList(((SubProcess) element).getFlowElements(), resultList); + } + } + } + + private void processFlowTaskExtList(List flowTaskExtList, BpmnModel bpmnModel) { + List elementList = new LinkedList<>(); + this.calculateAllElementList(bpmnModel.getMainProcess().getFlowElements(), elementList); + this.doAddLatestApprovalStatusListener(elementList); + Map elementMap = elementList.stream() + .filter(UserTask.class::isInstance).collect(Collectors.toMap(FlowElement::getId, c -> c)); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + for (FlowTaskExt t : flowTaskExtList) { + UserTask userTask = (UserTask) elementMap.get(t.getTaskId()); + flowApiService.addTaskCreateListener(userTask, FlowUserTaskListener.class); + Map> attributes = userTask.getAttributes(); + if (CollUtil.isNotEmpty(attributes.get(FlowConstant.USER_TASK_AUTO_SKIP_KEY))) { + flowApiService.addTaskCreateListener(userTask, AutoSkipTaskListener.class); + } + // 如果流程图中包含部门领导审批和上级部门领导审批的选项,就需要注册 FlowCustomExtFactory 工厂中的 + // BaseFlowIdentityExtHelper 对象,该注册操作需要业务模块中实现。 + if (StrUtil.equals(t.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + userTask.setCandidateGroups( + CollUtil.newArrayList("${" + FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR + "}")); + Assert.notNull(flowIdentityExtHelper); + flowApiService.addTaskCreateListener(userTask, flowIdentityExtHelper.getUpDeptPostLeaderListener()); + } else if (StrUtil.equals(t.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + userTask.setCandidateGroups( + CollUtil.newArrayList("${" + FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR + "}")); + Assert.notNull(flowIdentityExtHelper); + flowApiService.addTaskCreateListener(userTask, flowIdentityExtHelper.getDeptPostLeaderListener()); + } else if (StrUtil.equals(t.getGroupType(), FlowConstant.GROUP_TYPE_POST)) { + Assert.notNull(t.getDeptPostListJson()); + List groupDataList = + JSON.parseArray(t.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + List candidateGroupList = + FlowTaskPostCandidateGroup.buildCandidateGroupList(groupDataList); + userTask.setCandidateGroups(candidateGroupList); + } + this.processFlowTaskExtListener(userTask, t); + } + } + + private void processFlowTaskExtListener(UserTask userTask, FlowTaskExt taskExt) { + if (StrUtil.isBlank(taskExt.getExtraDataJson())) { + return; + } + FlowUserTaskExtData userTaskExtData = + JSON.parseObject(taskExt.getExtraDataJson(), FlowUserTaskExtData.class); + if (CollUtil.isNotEmpty(userTaskExtData.getFlowNotifyTypeList())) { + flowApiService.addTaskCreateListener(userTask, FlowTaskNotifyListener.class); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java new file mode 100644 index 00000000..bba2426f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowEntryVariableServiceImpl.java @@ -0,0 +1,137 @@ +package com.orangeforms.common.flow.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 流程变量数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowEntryVariableService") +public class FlowEntryVariableServiceImpl extends BaseService implements FlowEntryVariableService { + + @Autowired + private FlowEntryVariableMapper flowEntryVariableMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowEntryVariableMapper; + } + + /** + * 保存新增对象。 + * + * @param flowEntryVariable 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowEntryVariable saveNew(FlowEntryVariable flowEntryVariable) { + flowEntryVariable.setVariableId(idGenerator.nextLongId()); + flowEntryVariable.setCreateTime(new Date()); + flowEntryVariableMapper.insert(flowEntryVariable); + return flowEntryVariable; + } + + /** + * 更新数据对象。 + * + * @param flowEntryVariable 更新的对象。 + * @param originalFlowEntryVariable 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(FlowEntryVariable flowEntryVariable, FlowEntryVariable originalFlowEntryVariable) { + flowEntryVariable.setCreateTime(originalFlowEntryVariable.getCreateTime()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(flowEntryVariable, flowEntryVariable.getVariableId()); + return flowEntryVariableMapper.update(flowEntryVariable, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param variableId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long variableId) { + return flowEntryVariableMapper.deleteById(variableId) == 1; + } + + /** + * 删除指定流程Id的所有变量。 + * + * @param entryId 流程Id。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByEntryId(Long entryId) { + flowEntryVariableMapper.delete( + new LambdaQueryWrapper().eq(FlowEntryVariable::getEntryId, entryId)); + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getFlowEntryVariableListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getFlowEntryVariableList(FlowEntryVariable filter, String orderBy) { + return flowEntryVariableMapper.getFlowEntryVariableList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getFlowEntryVariableList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getFlowEntryVariableListWithRelation(FlowEntryVariable filter, String orderBy) { + List resultList = flowEntryVariableMapper.getFlowEntryVariableList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java new file mode 100644 index 00000000..5b0a86cc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMessageServiceImpl.java @@ -0,0 +1,385 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.model.constant.FlowMessageOperationType; +import com.orangeforms.common.flow.model.constant.FlowMessageType; +import com.orangeforms.common.flow.dao.FlowMessageIdentityOperationMapper; +import com.orangeforms.common.flow.dao.FlowMessageCandidateIdentityMapper; +import com.orangeforms.common.flow.dao.FlowMessageMapper; +import com.orangeforms.common.flow.object.FlowTaskPostCandidateGroup; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowMessageService; +import com.orangeforms.common.flow.service.FlowTaskExtService; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 工作流消息数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowMessageService") +public class FlowMessageServiceImpl extends BaseService implements FlowMessageService { + + @Autowired + private FlowMessageMapper flowMessageMapper; + @Autowired + private FlowMessageCandidateIdentityMapper flowMessageCandidateIdentityMapper; + @Autowired + private FlowMessageIdentityOperationMapper flowMessageIdentityOperationMapper; + @Autowired + private FlowTaskExtService flowTaskExtService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowMessageMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowMessage saveNew(FlowMessage flowMessage) { + flowMessage.setMessageId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData != null) { + flowMessage.setTenantId(tokenData.getTenantId()); + flowMessage.setAppCode(tokenData.getAppCode()); + flowMessage.setCreateUserId(tokenData.getUserId()); + flowMessage.setCreateUsername(tokenData.getShowName()); + flowMessage.setUpdateUserId(tokenData.getUserId()); + } + flowMessage.setCreateTime(new Date()); + flowMessage.setUpdateTime(flowMessage.getCreateTime()); + flowMessageMapper.insert(flowMessage); + return flowMessage; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewRemindMessage(FlowWorkOrder flowWorkOrder) { + List taskList = + flowApiService.getProcessInstanceActiveTaskList(flowWorkOrder.getProcessInstanceId()); + for (Task task : taskList) { + FlowMessage filter = new FlowMessage(); + filter.setTaskId(task.getId()); + List messageList = flowMessageMapper.selectList(new QueryWrapper<>(filter)); + // 同一个任务只能催办一次,多次催办则累加催办次数。 + if (CollUtil.isNotEmpty(messageList)) { + for (FlowMessage flowMessage : messageList) { + flowMessage.setRemindCount(flowMessage.getRemindCount() + 1); + flowMessageMapper.updateById(flowMessage); + } + continue; + } + FlowMessage flowMessage = BeanUtil.copyProperties(flowWorkOrder, FlowMessage.class); + flowMessage.setMessageType(FlowMessageType.REMIND_TYPE); + flowMessage.setRemindCount(1); + flowMessage.setProcessInstanceInitiator(flowWorkOrder.getSubmitUsername()); + flowMessage.setTaskId(task.getId()); + flowMessage.setTaskName(task.getName()); + flowMessage.setTaskStartTime(task.getCreateTime()); + flowMessage.setTaskAssignee(task.getAssignee()); + flowMessage.setTaskFinished(false); + if (TokenData.takeFromRequest() == null) { + Set usernameSet = CollUtil.newHashSet(flowWorkOrder.getSubmitUsername()); + Map m = flowCustomExtFactory.getFlowIdentityExtHelper().mapUserShowNameByLoginName(usernameSet); + flowMessage.setCreateUsername(m.containsKey(flowWorkOrder.getSubmitUsername()) + ? m.get(flowWorkOrder.getSubmitUsername()) : flowWorkOrder.getSubmitUsername()); + } + this.saveNew(flowMessage); + FlowTaskExt flowTaskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId( + flowWorkOrder.getProcessDefinitionId(), task.getTaskDefinitionKey()); + if (flowTaskExt != null) { + // 插入与当前消息关联任务的候选人 + this.saveMessageCandidateIdentityWithMessage( + flowWorkOrder.getProcessInstanceId(), flowTaskExt, task, flowMessage.getMessageId()); + } + // 插入与当前消息关联任务的指派人。 + if (StrUtil.isNotBlank(task.getAssignee())) { + this.saveMessageCandidateIdentity( + flowMessage.getMessageId(), FlowConstant.GROUP_TYPE_USER_VAR, task.getAssignee()); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewCopyMessage(Task task, JSONObject copyDataJson) { + if (copyDataJson.isEmpty()) { + return; + } + ProcessInstance instance = flowApiService.getProcessInstance(task.getProcessInstanceId()); + FlowMessage flowMessage = new FlowMessage(); + flowMessage.setMessageType(FlowMessageType.COPY_TYPE); + flowMessage.setRemindCount(0); + flowMessage.setProcessDefinitionId(instance.getProcessDefinitionId()); + flowMessage.setProcessDefinitionKey(instance.getProcessDefinitionKey()); + flowMessage.setProcessDefinitionName(instance.getProcessDefinitionName()); + flowMessage.setProcessInstanceId(instance.getProcessInstanceId()); + flowMessage.setProcessInstanceInitiator(instance.getStartUserId()); + flowMessage.setTaskId(task.getId()); + flowMessage.setTaskDefinitionKey(task.getTaskDefinitionKey()); + flowMessage.setTaskName(task.getName()); + flowMessage.setTaskStartTime(task.getCreateTime()); + flowMessage.setTaskAssignee(task.getAssignee()); + flowMessage.setTaskFinished(false); + flowMessage.setOnlineFormData(true); + // 如果是在线表单,这里就保存关联的在线表单Id,便于在线表单业务数据的查找。 + if (BooleanUtil.isTrue(flowMessage.getOnlineFormData())) { + TaskInfoVo taskInfo = JSON.parseObject(task.getFormKey(), TaskInfoVo.class); + flowMessage.setBusinessDataShot(taskInfo.getFormId().toString()); + } + this.saveNew(flowMessage); + for (Map.Entry entry : copyDataJson.entrySet()) { + if (entry.getValue() != null) { + this.saveMessageCandidateIdentityList( + flowMessage.getMessageId(), entry.getKey(), entry.getValue().toString()); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFinishedStatusByTaskId(String taskId) { + FlowMessage flowMessage = new FlowMessage(); + flowMessage.setTaskFinished(true); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowMessage::getTaskId, taskId); + flowMessageMapper.update(flowMessage, queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFinishedStatusByProcessInstanceId(String processInstanceId) { + FlowMessage flowMessage = new FlowMessage(); + flowMessage.setTaskFinished(true); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowMessage::getProcessInstanceId, processInstanceId); + flowMessageMapper.update(flowMessage, queryWrapper); + } + + @Override + public List getRemindingMessageListByUser() { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.getRemindingMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet()); + } + + @Override + public List getCopyMessageListByUser(Boolean read) { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.getCopyMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet(), read); + } + + @Override + public boolean isCandidateIdentityOnMessage(Long messageId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowMessageCandidateIdentity::getMessageId, messageId); + queryWrapper.in(FlowMessageCandidateIdentity::getCandidateId, buildGroupIdSet()); + return flowMessageCandidateIdentityMapper.selectCount(queryWrapper) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void readCopyTask(Long messageId) { + FlowMessageIdentityOperation operation = new FlowMessageIdentityOperation(); + operation.setId(idGenerator.nextLongId()); + operation.setMessageId(messageId); + operation.setLoginName(TokenData.takeFromRequest().getLoginName()); + operation.setOperationType(FlowMessageOperationType.READ_FINISHED); + operation.setOperationTime(new Date()); + flowMessageIdentityOperationMapper.insert(operation); + } + + @Override + public int countRemindingMessageListByUser() { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.countRemindingMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet()); + } + + @Override + public int countCopyMessageByUser() { + TokenData tokenData = TokenData.takeFromRequest(); + return flowMessageMapper.countCopyMessageListByUser( + tokenData.getTenantId(), tokenData.getAppCode(), tokenData.getLoginName(), buildGroupIdSet()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByProcessInstanceId(String processInstanceId) { + flowMessageCandidateIdentityMapper.deleteByProcessInstanceId(processInstanceId); + flowMessageIdentityOperationMapper.deleteByProcessInstanceId(processInstanceId); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowMessage::getProcessInstanceId, processInstanceId); + flowMessageMapper.delete(queryWrapper); + } + + private Set buildGroupIdSet() { + TokenData tokenData = TokenData.takeFromRequest(); + Set groupIdSet = new HashSet<>(1); + groupIdSet.add(tokenData.getLoginName()); + this.parseAndAddIdArray(groupIdSet, tokenData.getRoleIds()); + this.parseAndAddIdArray(groupIdSet, tokenData.getDeptPostIds()); + this.parseAndAddIdArray(groupIdSet, tokenData.getPostIds()); + if (tokenData.getDeptId() != null) { + groupIdSet.add(tokenData.getDeptId().toString()); + } + return groupIdSet; + } + + private void parseAndAddIdArray(Set groupIdSet, String idArray) { + if (StrUtil.isNotBlank(idArray)) { + if (groupIdSet == null) { + groupIdSet = new HashSet<>(); + } + groupIdSet.addAll(StrUtil.split(idArray, ',')); + } + } + + private void saveMessageCandidateIdentityWithMessage( + String processInstanceId, FlowTaskExt flowTaskExt, Task task, Long messageId) { + List candidates = flowApiService.getCandidateUsernames(flowTaskExt, task.getId()); + if (CollUtil.isNotEmpty(candidates)) { + this.saveMessageCandidateIdentityList( + messageId, FlowConstant.GROUP_TYPE_USER_VAR, CollUtil.join(candidates, ",")); + } + this.saveMessageCandidateIdentityList( + messageId, FlowConstant.GROUP_TYPE_ROLE_VAR, flowTaskExt.getRoleIds()); + this.saveMessageCandidateIdentityList( + messageId, FlowConstant.GROUP_TYPE_DEPT_VAR, flowTaskExt.getDeptIds()); + if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER)) { + Object v = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR); + if (v != null) { + this.saveMessageCandidateIdentity( + messageId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER)) { + Object v = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR); + if (v != null) { + this.saveMessageCandidateIdentity( + messageId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, v.toString()); + } + } else if (StrUtil.equals(flowTaskExt.getGroupType(), FlowConstant.GROUP_TYPE_POST)) { + Assert.notBlank(flowTaskExt.getDeptPostListJson()); + List groupDataList = + JSONArray.parseArray(flowTaskExt.getDeptPostListJson(), FlowTaskPostCandidateGroup.class); + for (FlowTaskPostCandidateGroup groupData : groupDataList) { + this.saveMessageCandidateIdentity(messageId, processInstanceId, groupData); + } + } + } + + private void saveMessageCandidateIdentity( + Long messageId, String processInstanceId, FlowTaskPostCandidateGroup groupData) { + FlowMessageCandidateIdentity candidateIdentity = new FlowMessageCandidateIdentity(); + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setMessageId(messageId); + candidateIdentity.setCandidateType(groupData.getType()); + switch (groupData.getType()) { + case FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR: + candidateIdentity.setCandidateId(groupData.getPostId()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_VAR: + candidateIdentity.setCandidateId(groupData.getDeptPostId()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + break; + case FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR: + Object v = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.SELF_DEPT_POST_PREFIX + groupData.getPostId()); + if (v != null) { + candidateIdentity.setCandidateId(v.toString()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR: + Object v2 = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.UP_DEPT_POST_PREFIX + groupData.getPostId()); + if (v2 != null) { + candidateIdentity.setCandidateId(v2.toString()); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + break; + case FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR: + Object v3 = flowApiService.getProcessInstanceVariable( + processInstanceId, FlowConstant.SIBLING_DEPT_POST_PREFIX + groupData.getPostId()); + if (v3 != null) { + List candidateIds = StrUtil.split(v3.toString(), ","); + for (String candidateId : candidateIds) { + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setCandidateId(candidateId); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + } + break; + default: + break; + } + } + private void saveMessageCandidateIdentity(Long messageId, String candidateType, String candidateId) { + FlowMessageCandidateIdentity candidateIdentity = new FlowMessageCandidateIdentity(); + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setMessageId(messageId); + candidateIdentity.setCandidateType(candidateType); + candidateIdentity.setCandidateId(candidateId); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + + private void saveMessageCandidateIdentityList(Long messageId, String candidateType, String identityIds) { + if (StrUtil.isNotBlank(identityIds)) { + for (String identityId : StrUtil.split(identityIds, ',')) { + FlowMessageCandidateIdentity candidateIdentity = new FlowMessageCandidateIdentity(); + candidateIdentity.setId(idGenerator.nextLongId()); + candidateIdentity.setMessageId(messageId); + candidateIdentity.setCandidateType(candidateType); + candidateIdentity.setCandidateId(identityId); + flowMessageCandidateIdentityMapper.insert(candidateIdentity); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java new file mode 100644 index 00000000..0266b624 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowMultiInstanceTransServiceImpl.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.flow.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.dao.FlowMultiInstanceTransMapper; +import com.orangeforms.common.flow.model.FlowMultiInstanceTrans; +import com.orangeforms.common.flow.service.FlowMultiInstanceTransService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +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.Date; + +/** + * 会签任务操作流水数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowMultiInstanceTransService") +public class FlowMultiInstanceTransServiceImpl + extends BaseService implements FlowMultiInstanceTransService { + + @Autowired + private FlowMultiInstanceTransMapper flowMultiInstanceTransMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowMultiInstanceTransMapper; + } + + /** + * 保存新增对象。 + * + * @param flowMultiInstanceTrans 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowMultiInstanceTrans saveNew(FlowMultiInstanceTrans flowMultiInstanceTrans) { + flowMultiInstanceTrans.setId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + flowMultiInstanceTrans.setCreateUserId(tokenData.getUserId()); + flowMultiInstanceTrans.setCreateLoginName(tokenData.getLoginName()); + flowMultiInstanceTrans.setCreateUsername(tokenData.getShowName()); + flowMultiInstanceTrans.setCreateTime(new Date()); + flowMultiInstanceTransMapper.insert(flowMultiInstanceTrans); + return flowMultiInstanceTrans; + } + + @Override + public FlowMultiInstanceTrans getByExecutionId(String executionId, String taskId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowMultiInstanceTrans::getExecutionId, executionId); + queryWrapper.eq(FlowMultiInstanceTrans::getTaskId, taskId); + return flowMultiInstanceTransMapper.selectOne(queryWrapper); + } + + @Override + public FlowMultiInstanceTrans getWithAssigneeListByMultiInstanceExecId(String multiInstanceExecId) { + if (multiInstanceExecId == null) { + return null; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowMultiInstanceTrans::getMultiInstanceExecId, multiInstanceExecId); + queryWrapper.isNotNull(FlowMultiInstanceTrans::getAssigneeList); + return flowMultiInstanceTransMapper.selectOne(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java new file mode 100644 index 00000000..a1244b61 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskCommentServiceImpl.java @@ -0,0 +1,142 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +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.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 流程任务批注数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowTaskCommentService") +public class FlowTaskCommentServiceImpl extends BaseService implements FlowTaskCommentService { + + @Autowired + private FlowTaskCommentMapper flowTaskCommentMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowTaskCommentMapper; + } + + /** + * 保存新增对象。 + * + * @param flowTaskComment 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowTaskComment saveNew(FlowTaskComment flowTaskComment) { + flowTaskComment.setId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData != null) { + flowTaskComment.setHeadImageUrl(tokenData.getHeadImageUrl()); + flowTaskComment.setCreateUserId(tokenData.getUserId()); + flowTaskComment.setCreateLoginName(tokenData.getLoginName()); + flowTaskComment.setCreateUsername(tokenData.getShowName()); + } + flowTaskComment.setCreateTime(new Date()); + flowTaskCommentMapper.insert(flowTaskComment); + FlowTaskComment.setToRequest(flowTaskComment); + return flowTaskComment; + } + + /** + * 查询指定流程实例Id下的所有审批任务的批注。 + * + * @param processInstanceId 流程实例Id。 + * @return 查询结果集。 + */ + @Override + public List getFlowTaskCommentList(String processInstanceId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.orderByAsc(FlowTaskComment::getId); + return flowTaskCommentMapper.selectList(queryWrapper); + } + + @Override + public List getFlowTaskCommentListByTaskIds(Set taskIdSet) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper().in(FlowTaskComment::getTaskId, taskIdSet); + queryWrapper.orderByDesc(FlowTaskComment::getId); + return flowTaskCommentMapper.selectList(queryWrapper); + } + + @Override + public FlowTaskComment getLatestFlowTaskComment(String processInstanceId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.orderByDesc(FlowTaskComment::getId); + IPage pageData = flowTaskCommentMapper.selectPage(new Page<>(1, 1), queryWrapper); + return CollUtil.isEmpty(pageData.getRecords()) ? null : pageData.getRecords().get(0); + } + + @Override + public FlowTaskComment getLatestFlowTaskComment(String processInstanceId, String taskDefinitionKey) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.eq(FlowTaskComment::getTaskKey, taskDefinitionKey); + queryWrapper.orderByDesc(FlowTaskComment::getId); + IPage pageData = flowTaskCommentMapper.selectPage(new Page<>(1, 1), queryWrapper); + return CollUtil.isEmpty(pageData.getRecords()) ? null : pageData.getRecords().get(0); + } + + @Override + public FlowTaskComment getFirstFlowTaskComment(String processInstanceId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.orderByAsc(FlowTaskComment::getId); + IPage pageData = flowTaskCommentMapper.selectPage(new Page<>(1, 1), queryWrapper); + return CollUtil.isEmpty(pageData.getRecords()) ? null : pageData.getRecords().get(0); + } + + @Override + public List getFlowTaskCommentListByExecutionId( + String processInstanceId, String taskId, String executionId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowTaskComment::getProcessInstanceId, processInstanceId); + queryWrapper.eq(FlowTaskComment::getTaskId, taskId); + queryWrapper.eq(FlowTaskComment::getExecutionId, executionId); + queryWrapper.orderByAsc(FlowTaskComment::getCreateTime); + return flowTaskCommentMapper.selectList(queryWrapper); + } + + @Override + public List getFlowTaskCommentListByMultiInstanceExecId(String multiInstanceExecId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowTaskComment::getMultiInstanceExecId, multiInstanceExecId); + return flowTaskCommentMapper.selectList(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java new file mode 100644 index 00000000..54bd7ac1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowTaskExtServiceImpl.java @@ -0,0 +1,622 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.Tuple2; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.object.FlowElementExtProperty; +import com.orangeforms.common.flow.object.FlowTaskMultiSignAssign; +import com.orangeforms.common.flow.object.FlowUserTaskExtData; +import com.orangeforms.common.flow.service.*; +import com.orangeforms.common.flow.dao.*; +import com.orangeforms.common.flow.model.*; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; +import org.flowable.task.api.TaskInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowTaskExtService") +public class FlowTaskExtServiceImpl extends BaseService implements FlowTaskExtService { + + @Autowired + private FlowTaskExtMapper flowTaskExtMapper; + @Autowired + private FlowEntryVariableService flowEntryVariableService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowMultiInstanceTransService flowMultiInstanceTransService; + @Autowired + private FlowTaskCommentService flowTaskCommentService; + + private static final String ID = "id"; + private static final String TYPE = "type"; + private static final String LABEL = "label"; + private static final String NAME = "name"; + private static final String VALUE = "value"; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowTaskExtMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveBatch(List flowTaskExtList) { + if (CollUtil.isNotEmpty(flowTaskExtList)) { + flowTaskExtMapper.insertList(flowTaskExtList); + } + } + + @Override + public FlowTaskExt getByProcessDefinitionIdAndTaskId(String processDefinitionId, String taskId) { + FlowTaskExt filter = new FlowTaskExt(); + filter.setProcessDefinitionId(processDefinitionId); + filter.setTaskId(taskId); + return flowTaskExtMapper.selectOne(new QueryWrapper<>(filter)); + } + + @Override + public List getByProcessDefinitionId(String processDefinitionId) { + FlowTaskExt filter = new FlowTaskExt(); + filter.setProcessDefinitionId(processDefinitionId); + return flowTaskExtMapper.selectList(new QueryWrapper<>(filter)); + } + + @Override + public List getCandidateUserInfoList( + String processInstanceId, + FlowTaskExt flowTaskExt, + TaskInfo taskInfo, + boolean isMultiInstanceTask, + boolean historic) { + List resultUserMapList = new LinkedList<>(); + if (!isMultiInstanceTask && this.buildTransferUserList(taskInfo, resultUserMapList)) { + return resultUserMapList; + } + Set loginNameSet = new HashSet<>(); + this.buildFlowUserInfoListByDeptAndRoleIds(flowTaskExt, loginNameSet, resultUserMapList); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + Set usernameSet = new HashSet<>(); + switch (flowTaskExt.getGroupType()) { + case FlowConstant.GROUP_TYPE_ASSIGNEE: + usernameSet.add(taskInfo.getAssignee()); + break; + case FlowConstant.GROUP_TYPE_DEPT_POST_LEADER: + String deptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + taskInfo.getExecutionId(), FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR); + List userInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(deptPostLeaderId)); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER: + String upDeptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + taskInfo.getExecutionId(), FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR); + List upUserInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(upDeptPostLeaderId)); + this.buildUserMapList(upUserInfoList, loginNameSet, resultUserMapList); + break; + default: + break; + } + List candidateUsernames = flowApiService.getCandidateUsernames(flowTaskExt, taskInfo.getId()); + if (CollUtil.isNotEmpty(candidateUsernames)) { + usernameSet.addAll(candidateUsernames); + } + if (isMultiInstanceTask) { + List assigneeList = this.getAssigneeList(taskInfo.getExecutionId(), taskInfo.getId()); + if (CollUtil.isNotEmpty(assigneeList)) { + usernameSet.addAll(assigneeList); + } + } + if (CollUtil.isNotEmpty(usernameSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByUsernameSet(usernameSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + Tuple2, Set> tuple2 = + flowApiService.getDeptPostIdAndPostIds(flowTaskExt, processInstanceId, historic); + Set postIdSet = tuple2.getSecond(); + Set deptPostIdSet = tuple2.getFirst(); + if (CollUtil.isNotEmpty(postIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByPostIds(postIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + if (CollUtil.isNotEmpty(deptPostIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByDeptPostIds(deptPostIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + return resultUserMapList; + } + + @Override + public List getCandidateUserInfoList( + String processInstanceId, + String executionId, + FlowTaskExt flowTaskExt) { + List resultUserMapList = new LinkedList<>(); + Set loginNameSet = new HashSet<>(); + this.buildFlowUserInfoListByDeptAndRoleIds(flowTaskExt, loginNameSet, resultUserMapList); + Set usernameSet = new HashSet<>(); + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + switch (flowTaskExt.getGroupType()) { + case FlowConstant.GROUP_TYPE_DEPT_POST_LEADER: + String deptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + executionId, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR); + List userInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(deptPostLeaderId)); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + break; + case FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER: + String upDeptPostLeaderId = flowApiService.getExecutionVariableStringWithSafe( + executionId, FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR); + List upUserInfoList = + flowIdentityExtHelper.getUserInfoListByDeptPostIds(CollUtil.newHashSet(upDeptPostLeaderId)); + this.buildUserMapList(upUserInfoList, loginNameSet, resultUserMapList); + break; + default: + break; + } + List candidateUsernames; + if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { + candidateUsernames = Collections.emptyList(); + } else { + if (!StrUtil.equals(flowTaskExt.getCandidateUsernames(), "${" + FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR + "}")) { + candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); + } else { + Object v = flowApiService.getExecutionVariableStringWithSafe(executionId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); + candidateUsernames = v == null ? null : StrUtil.split(v.toString(), ","); + } + } + if (CollUtil.isNotEmpty(candidateUsernames)) { + usernameSet.addAll(candidateUsernames); + } + if (CollUtil.isNotEmpty(usernameSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByUsernameSet(usernameSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + Tuple2, Set> tuple2 = + flowApiService.getDeptPostIdAndPostIds(flowTaskExt, processInstanceId, false); + Set postIdSet = tuple2.getSecond(); + Set deptPostIdSet = tuple2.getFirst(); + if (CollUtil.isNotEmpty(postIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByPostIds(postIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + if (CollUtil.isNotEmpty(deptPostIdSet)) { + List userInfoList = flowIdentityExtHelper.getUserInfoListByDeptPostIds(deptPostIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + return resultUserMapList; + } + + private void buildUserMapList( + List userInfoList, Set loginNameSet, List userMapList) { + if (CollUtil.isEmpty(userInfoList)) { + return; + } + for (FlowUserInfoVo userInfo : userInfoList) { + if (!loginNameSet.contains(userInfo.getLoginName())) { + loginNameSet.add(userInfo.getLoginName()); + userMapList.add(userInfo); + } + } + } + + @Override + public FlowTaskExt buildTaskExtByUserTask(UserTask userTask) { + FlowTaskExt flowTaskExt = new FlowTaskExt(); + flowTaskExt.setTaskId(userTask.getId()); + String formKey = userTask.getFormKey(); + if (StrUtil.isNotBlank(formKey)) { + TaskInfoVo taskInfoVo = JSON.parseObject(formKey, TaskInfoVo.class); + flowTaskExt.setGroupType(taskInfoVo.getGroupType()); + } + JSONObject extraDataJson = this.buildFlowTaskExtensionData(userTask); + if (extraDataJson != null) { + flowTaskExt.setExtraDataJson(extraDataJson.toJSONString()); + } + Map> extensionMap = userTask.getExtensionElements(); + if (MapUtil.isEmpty(extensionMap)) { + return flowTaskExt; + } + List operationList = this.buildOperationListExtensionElement(extensionMap); + if (CollUtil.isNotEmpty(operationList)) { + flowTaskExt.setOperationListJson(JSON.toJSONString(operationList)); + } + List variableList = this.buildVariableListExtensionElement(extensionMap); + if (CollUtil.isNotEmpty(variableList)) { + flowTaskExt.setVariableListJson(JSON.toJSONString(variableList)); + } + JSONObject assigneeListObject = this.buildAssigneeListExtensionElement(extensionMap); + if (assigneeListObject != null) { + flowTaskExt.setAssigneeListJson(JSON.toJSONString(assigneeListObject)); + } + List deptPostList = this.buildDeptPostListExtensionElement(extensionMap); + if (deptPostList != null) { + flowTaskExt.setDeptPostListJson(JSON.toJSONString(deptPostList)); + } + List copyList = this.buildCopyListExtensionElement(extensionMap); + if (copyList != null) { + flowTaskExt.setCopyListJson(JSON.toJSONString(copyList)); + } + JSONObject candidateGroupObject = this.buildUserCandidateGroupsExtensionElement(extensionMap); + if (candidateGroupObject != null) { + String type = candidateGroupObject.getString(TYPE); + String value = candidateGroupObject.getString(VALUE); + switch (type) { + case "DEPT": + flowTaskExt.setDeptIds(value); + break; + case "ROLE": + flowTaskExt.setRoleIds(value); + break; + case "USERS": + flowTaskExt.setCandidateUsernames(value); + break; + default: + break; + } + } + return flowTaskExt; + } + + @Override + public List buildTaskExtList(BpmnModel bpmnModel) { + List processList = bpmnModel.getProcesses(); + List flowTaskExtList = new LinkedList<>(); + for (Process process : processList) { + for (FlowElement element : process.getFlowElements()) { + this.doBuildTaskExtList(element, flowTaskExtList); + } + } + return flowTaskExtList; + } + + @Override + public List buildOperationListExtensionElement(Map> extensionMap) { + List formOperationElements = + this.getMyExtensionElementList(extensionMap, "operationList", "formOperation"); + if (CollUtil.isEmpty(formOperationElements)) { + return Collections.emptyList(); + } + List resultList = new LinkedList<>(); + for (ExtensionElement e : formOperationElements) { + JSONObject operationJsonData = new JSONObject(); + operationJsonData.put(ID, e.getAttributeValue(null, ID)); + operationJsonData.put(LABEL, e.getAttributeValue(null, LABEL)); + operationJsonData.put(TYPE, e.getAttributeValue(null, TYPE)); + operationJsonData.put("showOrder", e.getAttributeValue(null, "showOrder")); + operationJsonData.put("latestApprovalStatus", e.getAttributeValue(null, "latestApprovalStatus")); + String multiSignAssignee = e.getAttributeValue(null, "multiSignAssignee"); + if (StrUtil.isNotBlank(multiSignAssignee)) { + operationJsonData.put("multiSignAssignee", + JSON.parseObject(multiSignAssignee, FlowTaskMultiSignAssign.class)); + } + resultList.add(operationJsonData); + } + return resultList; + } + + @Override + public List buildVariableListExtensionElement(Map> extensionMap) { + List formVariableElements = + this.getMyExtensionElementList(extensionMap, "variableList", "formVariable"); + if (CollUtil.isEmpty(formVariableElements)) { + return Collections.emptyList(); + } + Set variableIdSet = new HashSet<>(); + for (ExtensionElement e : formVariableElements) { + String id = e.getAttributeValue(null, ID); + variableIdSet.add(Long.parseLong(id)); + } + List variableList = flowEntryVariableService.getInList(variableIdSet); + List resultList = new LinkedList<>(); + for (FlowEntryVariable variable : variableList) { + resultList.add((JSONObject) JSON.toJSON(variable)); + } + return resultList; + } + + @Override + public FlowElementExtProperty buildFlowElementExt(FlowElement element) { + JSONObject propertiesData = this.buildFlowElementExtToJson(element); + return propertiesData == null ? null : propertiesData.toJavaObject(FlowElementExtProperty.class); + } + + @Override + public JSONObject buildFlowElementExtToJson(FlowElement element) { + Map> extensionMap = element.getExtensionElements(); + List propertiesElements = + this.getMyExtensionElementList(extensionMap, "properties", "property"); + if (CollUtil.isEmpty(propertiesElements)) { + return null; + } + JSONObject propertiesData = new JSONObject(); + for (ExtensionElement e : propertiesElements) { + String name = e.getAttributeValue(null, NAME); + String value = e.getAttributeValue(null, VALUE); + propertiesData.put(name, value); + } + return propertiesData; + } + + private void doBuildTaskExtList(FlowElement element, List flowTaskExtList) { + if (element instanceof UserTask) { + FlowTaskExt flowTaskExt = this.buildTaskExtByUserTask((UserTask) element); + flowTaskExtList.add(flowTaskExt); + } else if (element instanceof SubProcess) { + Collection flowElements = ((SubProcess) element).getFlowElements(); + for (FlowElement element1 : flowElements) { + this.doBuildTaskExtList(element1, flowTaskExtList); + } + } + } + + private void buildFlowUserInfoListByDeptAndRoleIds( + FlowTaskExt flowTaskExt, Set loginNameSet, List resultUserMapList) { + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + if (StrUtil.isNotBlank(flowTaskExt.getDeptIds())) { + Set deptIdSet = CollUtil.newHashSet(StrUtil.split(flowTaskExt.getDeptIds(), ',')); + List userInfoList = flowIdentityExtHelper.getUserInfoListByDeptIds(deptIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + if (StrUtil.isNotBlank(flowTaskExt.getRoleIds())) { + Set roleIdSet = CollUtil.newHashSet(StrUtil.split(flowTaskExt.getRoleIds(), ',')); + List userInfoList = flowIdentityExtHelper.getUserInfoListByRoleIds(roleIdSet); + this.buildUserMapList(userInfoList, loginNameSet, resultUserMapList); + } + } + + private void buildFlowTaskTimeoutExtensionData( + Map> attributeMap, JSONObject extraDataJson) { + List timeoutHandleWayAttributes = attributeMap.get(FlowConstant.TASK_TIMEOUT_HANDLE_WAY); + if (CollUtil.isNotEmpty(timeoutHandleWayAttributes)) { + String handleWay = timeoutHandleWayAttributes.get(0).getValue(); + extraDataJson.put(FlowConstant.TASK_TIMEOUT_HANDLE_WAY, handleWay); + List timeoutHoursAttributes = attributeMap.get(FlowConstant.TASK_TIMEOUT_HOURS); + if (CollUtil.isEmpty(timeoutHoursAttributes)) { + throw new MyRuntimeException("没有设置任务超时小时数!"); + } + Integer timeoutHours = Integer.valueOf(timeoutHoursAttributes.get(0).getValue()); + extraDataJson.put(FlowConstant.TASK_TIMEOUT_HOURS, timeoutHours); + if (StrUtil.equals(handleWay, FlowUserTaskExtData.TIMEOUT_AUTO_COMPLETE)) { + List defaultAssigneeAttributes = + attributeMap.get(FlowConstant.TASK_TIMEOUT_DEFAULT_ASSIGNEE); + if (CollUtil.isEmpty(defaultAssigneeAttributes)) { + throw new MyRuntimeException("没有设置超时任务处理人!"); + } + extraDataJson.put(FlowConstant.TASK_TIMEOUT_DEFAULT_ASSIGNEE, defaultAssigneeAttributes.get(0).getValue()); + } + } + } + + private void buildFlowTaskEmptyUserExtensionData( + Map> attributeMap, JSONObject extraDataJson) { + List emptyUserHandleWayAttributes = attributeMap.get(FlowConstant.EMPTY_USER_HANDLE_WAY); + if (CollUtil.isNotEmpty(emptyUserHandleWayAttributes)) { + String handleWay = emptyUserHandleWayAttributes.get(0).getValue(); + extraDataJson.put(FlowConstant.EMPTY_USER_HANDLE_WAY, handleWay); + if (StrUtil.equals(handleWay, FlowUserTaskExtData.EMPTY_USER_TO_ASSIGNEE)) { + List emptyUserToAssigneeAttributes = attributeMap.get(FlowConstant.EMPTY_USER_TO_ASSIGNEE); + if (CollUtil.isEmpty(emptyUserToAssigneeAttributes)) { + throw new MyRuntimeException("没有设置空审批人的指定处理人!"); + } + extraDataJson.put(FlowConstant.EMPTY_USER_TO_ASSIGNEE, emptyUserToAssigneeAttributes.get(0).getValue()); + } + } + } + + private JSONObject buildFlowTaskExtensionData(UserTask userTask) { + JSONObject extraDataJson = this.buildFlowElementExtToJson(userTask); + Map> attributeMap = userTask.getAttributes(); + if (MapUtil.isEmpty(attributeMap)) { + return extraDataJson; + } + if (extraDataJson == null) { + extraDataJson = new JSONObject(); + } + this.buildFlowTaskTimeoutExtensionData(attributeMap, extraDataJson); + this.buildFlowTaskEmptyUserExtensionData(attributeMap, extraDataJson); + List rejectTypeAttributes = attributeMap.get(FlowConstant.USER_TASK_REJECT_TYPE_KEY); + if (CollUtil.isNotEmpty(rejectTypeAttributes)) { + extraDataJson.put(FlowConstant.USER_TASK_REJECT_TYPE_KEY, rejectTypeAttributes.get(0).getValue()); + } + List sendMsgTypeAttributes = attributeMap.get("sendMessageType"); + if (CollUtil.isNotEmpty(sendMsgTypeAttributes)) { + ExtensionAttribute attribute = sendMsgTypeAttributes.get(0); + extraDataJson.put(FlowConstant.USER_TASK_NOTIFY_TYPES_KEY, StrUtil.split(attribute.getValue(), ",")); + } + return extraDataJson; + } + + private JSONObject buildUserCandidateGroupsExtensionElement(Map> extensionMap) { + JSONObject jsonData = null; + List elementCandidateGroupsList = extensionMap.get("userCandidateGroups"); + if (CollUtil.isEmpty(elementCandidateGroupsList)) { + return jsonData; + } + jsonData = new JSONObject(); + ExtensionElement ee = elementCandidateGroupsList.get(0); + jsonData.put(TYPE, ee.getAttributeValue(null, TYPE)); + jsonData.put(VALUE, ee.getAttributeValue(null, VALUE)); + return jsonData; + } + + private JSONObject buildAssigneeListExtensionElement(Map> extensionMap) { + JSONObject jsonData = null; + List elementAssigneeList = extensionMap.get("assigneeList"); + if (CollUtil.isEmpty(elementAssigneeList)) { + return jsonData; + } + ExtensionElement ee = elementAssigneeList.get(0); + Map> childExtensionMap = ee.getChildElements(); + if (MapUtil.isEmpty(childExtensionMap)) { + return jsonData; + } + List assigneeElements = childExtensionMap.get("assignee"); + if (CollUtil.isEmpty(assigneeElements)) { + return jsonData; + } + JSONArray assigneeIdArray = new JSONArray(); + for (ExtensionElement e : assigneeElements) { + assigneeIdArray.add(e.getAttributeValue(null, ID)); + } + jsonData = new JSONObject(); + String assigneeType = ee.getAttributeValue(null, TYPE); + jsonData.put("assigneeType", assigneeType); + jsonData.put("assigneeList", assigneeIdArray); + return jsonData; + } + + private List buildDeptPostListExtensionElement(Map> extensionMap) { + List deptPostElements = + this.getMyExtensionElementList(extensionMap, "deptPostList", "deptPost"); + if (CollUtil.isEmpty(deptPostElements)) { + return Collections.emptyList(); + } + List resultList = new LinkedList<>(); + for (ExtensionElement e : deptPostElements) { + JSONObject deptPostJsonData = new JSONObject(); + deptPostJsonData.put(ID, e.getAttributeValue(null, ID)); + deptPostJsonData.put(TYPE, e.getAttributeValue(null, TYPE)); + String postId = e.getAttributeValue(null, "postId"); + if (postId != null) { + deptPostJsonData.put("postId", postId); + } + String deptPostId = e.getAttributeValue(null, "deptPostId"); + if (deptPostId != null) { + deptPostJsonData.put("deptPostId", deptPostId); + } + resultList.add(deptPostJsonData); + } + return resultList; + } + + private List buildCopyListExtensionElement(Map> extensionMap) { + List copyElements = + this.getMyExtensionElementList(extensionMap, "copyItemList", "copyItem"); + if (CollUtil.isEmpty(copyElements)) { + return Collections.emptyList(); + } + List resultList = new LinkedList<>(); + for (ExtensionElement e : copyElements) { + JSONObject copyJsonData = new JSONObject(); + String type = e.getAttributeValue(null, TYPE); + copyJsonData.put(TYPE, type); + if (!StrUtil.equalsAny(type, FlowConstant.GROUP_TYPE_DEPT_POST_LEADER_VAR, + FlowConstant.GROUP_TYPE_UP_DEPT_POST_LEADER_VAR, + FlowConstant.GROUP_TYPE_USER_VAR, + FlowConstant.GROUP_TYPE_ROLE_VAR, + FlowConstant.GROUP_TYPE_DEPT_VAR, + FlowConstant.GROUP_TYPE_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_ALL_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_SIBLING_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_SELF_DEPT_POST_VAR, + FlowConstant.GROUP_TYPE_UP_DEPT_POST_VAR)) { + throw new MyRuntimeException("Invalid TYPE [" + type + " ] for CopyItenList Extension!"); + } + String id = e.getAttributeValue(null, ID); + if (StrUtil.isNotBlank(id)) { + copyJsonData.put(ID, id); + } + resultList.add(copyJsonData); + } + return resultList; + } + + private List getMyExtensionElementList( + Map> extensionMap, String rootName, String childName) { + if (extensionMap == null) { + return Collections.emptyList(); + } + List elementList = extensionMap.get(rootName); + if (CollUtil.isEmpty(elementList)) { + return Collections.emptyList(); + } + if (StrUtil.isBlank(childName)) { + return elementList; + } + ExtensionElement ee = elementList.get(0); + Map> childExtensionMap = ee.getChildElements(); + if (MapUtil.isEmpty(childExtensionMap)) { + return Collections.emptyList(); + } + List childrenElements = childExtensionMap.get(childName); + if (CollUtil.isEmpty(childrenElements)) { + return Collections.emptyList(); + } + return childrenElements; + } + + private List getAssigneeList(String executionId, String taskId) { + FlowMultiInstanceTrans flowMultiInstanceTrans = + flowMultiInstanceTransService.getByExecutionId(executionId, taskId); + String multiInstanceExecId; + if (flowMultiInstanceTrans == null) { + multiInstanceExecId = flowApiService.getTaskVariableStringWithSafe( + taskId, FlowConstant.MULTI_SIGN_TASK_EXECUTION_ID_VAR); + } else { + multiInstanceExecId = flowMultiInstanceTrans.getMultiInstanceExecId(); + } + flowMultiInstanceTrans = + flowMultiInstanceTransService.getWithAssigneeListByMultiInstanceExecId(multiInstanceExecId); + return flowMultiInstanceTrans == null ? null + : StrUtil.split(flowMultiInstanceTrans.getAssigneeList(), ","); + } + + private boolean buildTransferUserList(TaskInfo taskInfo, List resultUserMapList) { + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + List taskCommentList = flowTaskCommentService.getFlowTaskCommentListByExecutionId( + taskInfo.getProcessInstanceId(), taskInfo.getId(), taskInfo.getExecutionId()); + if (CollUtil.isEmpty(taskCommentList)) { + return false; + } + FlowTaskComment transferComment = null; + for (int i = taskCommentList.size() - 1; i >= 0; i--) { + FlowTaskComment comment = taskCommentList.get(i); + if (StrUtil.equalsAny(comment.getApprovalType(), + FlowApprovalType.TRANSFER, FlowApprovalType.INTERVENE)) { + transferComment = comment; + break; + } + } + if (transferComment == null || StrUtil.isBlank(transferComment.getDelegateAssignee())) { + return false; + } + Set loginNameSet = new HashSet<>(StrUtil.split(transferComment.getDelegateAssignee(), ",")); + resultUserMapList.addAll(flowIdentityExtHelper.getUserInfoListByUsernameSet(loginNameSet)); + return true; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java new file mode 100644 index 00000000..b36785a9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/service/impl/FlowWorkOrderServiceImpl.java @@ -0,0 +1,356 @@ +package com.orangeforms.common.flow.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.annotation.DisableDataFilter; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.dao.FlowWorkOrderExtMapper; +import com.orangeforms.common.flow.dao.FlowWorkOrderMapper; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.model.FlowEntry; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.FlowWorkOrderExt; +import com.orangeforms.common.flow.util.FlowOperationHelper; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.util.BaseFlowIdentityExtHelper; +import com.orangeforms.common.flow.util.FlowCustomExtFactory; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("flowWorkOrderService") +public class FlowWorkOrderServiceImpl extends BaseService implements FlowWorkOrderService { + + @Autowired + private FlowWorkOrderMapper flowWorkOrderMapper; + @Autowired + private FlowWorkOrderExtMapper flowWorkOrderExtMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private FlowOperationHelper flowOperationHelper; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return flowWorkOrderMapper; + } + + /** + * 保存新增对象。 + * + * @param instance 流程实例对象。 + * @param dataId 流程实例的BusinessKey。 + * @param onlineTableId 在线数据表的主键Id。 + * @param tableName 面向静态表单所使用的表名。 + * @return 新增的工作流工单对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public FlowWorkOrder saveNew(ProcessInstance instance, Object dataId, Long onlineTableId, String tableName) { + // 正常插入流程工单数据。 + FlowWorkOrder flowWorkOrder = this.createWith(instance); + flowWorkOrder.setWorkOrderCode(this.generateWorkOrderCode(instance.getProcessDefinitionKey())); + flowWorkOrder.setBusinessKey(dataId.toString()); + flowWorkOrder.setOnlineTableId(onlineTableId); + flowWorkOrder.setTableName(tableName); + flowWorkOrder.setFlowStatus(FlowTaskStatus.SUBMITTED); + flowWorkOrderMapper.insert(flowWorkOrder); + return flowWorkOrder; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public FlowWorkOrder saveNewWithDraft( + ProcessInstance instance, Long onlineTableId, String tableName, String masterData, String slaveData) { + FlowWorkOrder flowWorkOrder = this.createWith(instance); + flowWorkOrder.setWorkOrderCode(this.generateWorkOrderCode(instance.getProcessDefinitionKey())); + flowWorkOrder.setOnlineTableId(onlineTableId); + flowWorkOrder.setTableName(tableName); + flowWorkOrder.setFlowStatus(FlowTaskStatus.DRAFT); + JSONObject draftData = new JSONObject(); + if (masterData != null) { + draftData.put(FlowConstant.MASTER_DATA_KEY, masterData); + } + if (slaveData != null) { + draftData.put(FlowConstant.SLAVE_DATA_KEY, slaveData); + } + FlowWorkOrderExt flowWorkOrderExt = + BeanUtil.copyProperties(flowWorkOrder, FlowWorkOrderExt.class); + flowWorkOrderExt.setId(idGenerator.nextLongId()); + flowWorkOrderExt.setDraftData(JSON.toJSONString(draftData)); + flowWorkOrderExtMapper.insert(flowWorkOrderExt); + flowWorkOrderMapper.insert(flowWorkOrder); + return flowWorkOrder; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateDraft(Long workOrderId, String masterData, String slaveData) { + JSONObject draftData = new JSONObject(); + if (masterData != null) { + draftData.put(FlowConstant.MASTER_DATA_KEY, masterData); + } + if (slaveData != null) { + draftData.put(FlowConstant.SLAVE_DATA_KEY, slaveData); + } + FlowWorkOrderExt flowWorkOrderExt = new FlowWorkOrderExt(); + flowWorkOrderExt.setDraftData(JSON.toJSONString(draftData)); + flowWorkOrderExt.setUpdateTime(new Date()); + flowWorkOrderExtMapper.update(flowWorkOrderExt, + new LambdaQueryWrapper().eq(FlowWorkOrderExt::getWorkOrderId, workOrderId)); + } + + /** + * 删除指定数据。 + * + * @param workOrderId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long workOrderId) { + return flowWorkOrderMapper.deleteById(workOrderId) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByProcessInstanceId(String processInstanceId) { + FlowWorkOrder filter = new FlowWorkOrder(); + filter.setProcessInstanceId(processInstanceId); + super.removeBy(filter); + } + + @Override + public List getFlowWorkOrderList(FlowWorkOrder filter, String orderBy) { + if (filter == null) { + filter = new FlowWorkOrder(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return flowWorkOrderMapper.getFlowWorkOrderList(filter, orderBy); + } + + @Override + public List getFlowWorkOrderListWithRelation(FlowWorkOrder filter, String orderBy) { + List resultList = this.getFlowWorkOrderList(filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + @Override + public FlowWorkOrder getFlowWorkOrderByProcessInstanceId(String processInstanceId) { + FlowWorkOrder filter = new FlowWorkOrder(); + filter.setProcessInstanceId(processInstanceId); + return flowWorkOrderMapper.selectOne(new QueryWrapper<>(filter)); + } + + @Override + public boolean existByBusinessKey(String tableName, Object businessKey, boolean unfinished) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowWorkOrder::getBusinessKey, businessKey.toString()); + queryWrapper.eq(FlowWorkOrder::getTableName, tableName); + if (unfinished) { + queryWrapper.notIn(FlowWorkOrder::getFlowStatus, + FlowTaskStatus.FINISHED, FlowTaskStatus.CANCELLED, FlowTaskStatus.STOPPED); + } + return flowWorkOrderMapper.selectCount(queryWrapper) > 0; + } + + @Override + public boolean existUnfinished(String processDefinitionKey, Object businessKey) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowWorkOrder::getBusinessKey, businessKey.toString()); + queryWrapper.eq(FlowWorkOrder::getProcessDefinitionKey, processDefinitionKey); + queryWrapper.notIn(FlowWorkOrder::getFlowStatus, + FlowTaskStatus.FINISHED, FlowTaskStatus.CANCELLED, FlowTaskStatus.STOPPED); + return flowWorkOrderMapper.selectCount(queryWrapper) > 0; + } + + @DisableDataFilter + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFlowStatusByProcessInstanceId(String processInstanceId, Integer flowStatus) { + if (flowStatus == null) { + return; + } + FlowWorkOrder flowWorkOrder = new FlowWorkOrder(); + flowWorkOrder.setFlowStatus(flowStatus); + if (FlowTaskStatus.FINISHED != flowStatus) { + flowWorkOrder.setUpdateTime(new Date()); + flowWorkOrder.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FlowWorkOrder::getProcessInstanceId, processInstanceId); + flowWorkOrderMapper.update(flowWorkOrder, queryWrapper); + } + + @DisableDataFilter + @Transactional(rollbackFor = Exception.class) + @Override + public void updateLatestApprovalStatusByProcessInstanceId(String processInstanceId, Integer approvalStatus) { + if (approvalStatus == null) { + return; + } + FlowWorkOrder flowWorkOrder = this.getFlowWorkOrderByProcessInstanceId(processInstanceId); + flowWorkOrder.setLatestApprovalStatus(approvalStatus); + flowWorkOrder.setUpdateTime(new Date()); + flowWorkOrder.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + flowWorkOrderMapper.updateById(flowWorkOrder); + flowCustomExtFactory.getOnlineBusinessDataExtHelper().updateFlowStatus(flowWorkOrder); + // 处理在线表单工作流的自定义状态更新。 + flowCustomExtFactory.getOnlineBusinessDataExtHelper().updateFlowStatus(flowWorkOrder); + } + + @Override + public boolean hasDataPermOnFlowWorkOrder(String processInstanceId) { + // 开启数据权限,并进行验证。 + boolean originalFlag = GlobalThreadLocal.setDataFilter(true); + long count; + try { + FlowWorkOrder filter = new FlowWorkOrder(); + filter.setProcessInstanceId(processInstanceId); + count = flowWorkOrderMapper.selectCount(new QueryWrapper<>(filter)); + } finally { + // 恢复之前的数据权限标记 + GlobalThreadLocal.setDataFilter(originalFlag); + } + return count > 0; + } + + @Override + public void fillUserShowNameByLoginName(List dataList) { + BaseFlowIdentityExtHelper identityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + Set loginNameSet = dataList.stream() + .map(FlowWorkOrderVo::getSubmitUsername).collect(Collectors.toSet()); + if (CollUtil.isEmpty(loginNameSet)) { + return; + } + Map userNameMap = identityExtHelper.mapUserShowNameByLoginName(loginNameSet); + dataList.forEach(workOrder -> { + if (StrUtil.isNotBlank(workOrder.getSubmitUsername())) { + workOrder.setUserShowName(userNameMap.get(workOrder.getSubmitUsername())); + } + }); + } + + @Override + public FlowWorkOrderExt getFlowWorkOrderExtByWorkOrderId(Long workOrderId) { + return flowWorkOrderExtMapper.selectOne( + new LambdaQueryWrapper().eq(FlowWorkOrderExt::getWorkOrderId, workOrderId)); + } + + @Override + public List getFlowWorkOrderExtByWorkOrderIds(Set workOrderIds) { + return flowWorkOrderExtMapper.selectList( + new LambdaQueryWrapper().in(FlowWorkOrderExt::getWorkOrderId, workOrderIds)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public CallResult removeDraft(FlowWorkOrder flowWorkOrder) { + CallResult r = flowApiService.stopProcessInstance(flowWorkOrder.getProcessInstanceId(), "撤销草稿", true); + if (!r.isSuccess()) { + return r; + } + flowWorkOrderMapper.deleteById(flowWorkOrder.getWorkOrderId()); + return CallResult.ok(); + } + + @Override + public MyPageData getPagedWorkOrderListAndBuildData( + FlowWorkOrderDto flowWorkOrderDtoFilter, MyPageParam pageParam, MyOrderParam orderParam, String processDefinitionKey) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount()); + String orderBy = MyOrderParam.buildOrderBy(orderParam, FlowWorkOrder.class); + FlowWorkOrder filter = flowOperationHelper.makeWorkOrderFilter(flowWorkOrderDtoFilter, processDefinitionKey); + List flowWorkOrderList = this.getFlowWorkOrderList(filter, orderBy); + MyPageData resultData = + MyPageUtil.makeResponseData(flowWorkOrderList, FlowWorkOrderVo.class); + if (CollUtil.isEmpty(resultData.getDataList())) { + return resultData; + } + flowOperationHelper.buildWorkOrderApprovalStatus(processDefinitionKey, resultData.getDataList()); + // 根据工单的提交用户名获取用户的显示名称,便于前端显示。 + // 同时这也是一个如何通过插件方法,将loginName映射到showName的示例, + this.fillUserShowNameByLoginName(resultData.getDataList()); + // 组装工单中需要返回给前端的流程任务数据。 + flowOperationHelper.buildWorkOrderTaskInfo(resultData.getDataList()); + return resultData; + } + + private FlowWorkOrder createWith(ProcessInstance instance) { + TokenData tokenData = TokenData.takeFromRequest(); + Date now = new Date(); + FlowWorkOrder flowWorkOrder = new FlowWorkOrder(); + flowWorkOrder.setWorkOrderId(idGenerator.nextLongId()); + flowWorkOrder.setProcessDefinitionKey(instance.getProcessDefinitionKey()); + flowWorkOrder.setProcessDefinitionName(instance.getProcessDefinitionName()); + flowWorkOrder.setProcessDefinitionId(instance.getProcessDefinitionId()); + flowWorkOrder.setProcessInstanceId(instance.getId()); + flowWorkOrder.setSubmitUsername(tokenData.getLoginName()); + flowWorkOrder.setDeptId(tokenData.getDeptId()); + flowWorkOrder.setAppCode(tokenData.getAppCode()); + flowWorkOrder.setTenantId(tokenData.getTenantId()); + flowWorkOrder.setCreateUserId(tokenData.getUserId()); + flowWorkOrder.setUpdateUserId(tokenData.getUserId()); + flowWorkOrder.setCreateTime(now); + flowWorkOrder.setUpdateTime(now); + flowWorkOrder.setDeletedFlag(GlobalDeletedFlag.NORMAL); + return flowWorkOrder; + } + + private String generateWorkOrderCode(String processDefinitionKey) { + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (StrUtil.isBlank(flowEntry.getEncodedRule())) { + return null; + } + ColumnEncodedRule rule = JSON.parseObject(flowEntry.getEncodedRule(), ColumnEncodedRule.class); + if (rule.getIdWidth() == null) { + rule.setIdWidth(10); + } + return commonRedisUtil.generateTransId( + rule.getPrefix(), rule.getPrecisionTo(), rule.getMiddle(), rule.getIdWidth()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java new file mode 100644 index 00000000..30715c8c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowIdentityExtHelper.java @@ -0,0 +1,253 @@ +package com.orangeforms.common.flow.util; + +import com.orangeforms.common.flow.listener.DeptPostLeaderListener; +import com.orangeforms.common.flow.listener.UpDeptPostLeaderListener; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import org.flowable.engine.delegate.TaskListener; + +import java.util.*; + +/** + * 工作流与用户身份相关的自定义扩展接口,需要业务模块自行实现该接口。也可以根据实际需求扩展该接口的方法。 + * 目前支持的主键类型为字符型和长整型,所以这里提供了两套实现接口。可根据实际情况实现其中一套即可。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface BaseFlowIdentityExtHelper { + + /** + * 根据(字符型)部门Id,获取当前用户部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户部门领导所有的部门岗位Id。 + */ + default String getLeaderDeptPostId(String deptId) { + return null; + } + + /** + * 根据(字符型)部门Id,获取当前用户上级部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户上级部门领导所有的部门岗位Id。 + */ + default String getUpLeaderDeptPostId(String deptId) { + return null; + } + + /** + * 获取(字符型)指定部门上级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与该部门Id上级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getUpDeptPostIdMap(String deptId, Set postIdSet) { + return null; + } + + /** + * 获取(字符型)指定部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getDeptPostIdMap(String deptId, Set postIdSet) { + return null; + } + + /** + * 获取(字符型)指定同级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的同级部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与同级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getSiblingDeptPostIdMap(String deptId, Set postIdSet) { + return null; + } + + /** + * 根据(长整型)部门Id,获取当前用户部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户部门领导所有的部门岗位Id。 + */ + default Long getLeaderDeptPostId(Long deptId) { + return null; + } + + /** + * 根据(长整型)部门Id,获取当前用户上级部门领导所有的部门岗位Id。 + * + * @param deptId 用户所在部门Id。 + * @return 当前用户上级部门领导所有的部门岗位Id。 + */ + default Long getUpLeaderDeptPostId(Long deptId) { + return null; + } + + /** + * 获取(长整型)指定部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getDeptPostIdMap(Long deptId, Set postIdSet) { + return null; + } + + /** + * 获取(长整型)指定同级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的同级部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与同级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getSiblingDeptPostIdMap(Long deptId, Set postIdSet) { + return null; + } + + /** + * 获取(长整型)指定部门上级部门的指定岗位集合的DeptPostId集合。 + * + * @param deptId 指定的部门Id。 + * @param postIdSet 指定的岗位Id集合。 + * @return 与该部门Id上级部门关联的岗位Id集合,key对应参数中的postId,value是与key对应的deptPostId。 + */ + default Map getUpDeptPostIdMap(Long deptId, Set postIdSet) { + return null; + } + + /** + * 根据角色Id集合,查询所属的用户名列表。 + * + * @param roleIdSet 角色Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByRoleIds(Set roleIdSet) { + return Collections.emptySet(); + } + + /** + * 根据角色Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param roleIdSet 角色Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByRoleIds(Set roleIdSet) { + return Collections.emptyList(); + } + + /** + * 根据部门Id集合,查询所属的用户名列表。 + * + * @param deptIdSet 部门Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByDeptIds(Set deptIdSet) { + return Collections.emptySet(); + } + + /** + * 根据部门Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param deptIdSet 部门Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByDeptIds(Set deptIdSet) { + return Collections.emptyList(); + } + + /** + * 根据岗位Id集合,查询所属的用户名列表。 + * + * @param postIdSet 岗位Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByPostIds(Set postIdSet) { + return Collections.emptySet(); + } + + /** + * 根据岗位Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param postIdSet 岗位Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByPostIds(Set postIdSet) { + return Collections.emptyList(); + } + + /** + * 根据部门岗位Id集合,查询所属的用户名列表。 + * + * @param deptPostIdSet 部门岗位Id集合。 + * @return 所属的用户列表。 + */ + default Set getUsernameListByDeptPostIds(Set deptPostIdSet) { + return Collections.emptySet(); + } + + /** + * 根据部门岗位Id集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param deptPostIdSet 部门岗位Id集合。 + * @return 所属的用户对象信息列表。 + */ + default List getUserInfoListByDeptPostIds(Set deptPostIdSet) { + return Collections.emptyList(); + } + + /** + * 根据用户登录名集合,查询所属的用户对象信息列表。返回的具体数据,用户可自定义。 + * + * @param usernameSet 用户登录名集合。 + * @return 用户对象信息列表。 + */ + default List getUserInfoListByUsernameSet(Set usernameSet) { + return Collections.emptyList(); + } + + /** + * 当前服务是否支持数据权限。 + * + * @return true表示支持,否则false。 + */ + default Boolean supprtDataPerm() { + return false; + } + + /** + * 映射用户的登录名到用户的显示名。 + * + * @param loginNameSet 用户登录名集合。 + * @return 用户登录名和显示名的Map,key为登录名,value是显示名。 + */ + default Map mapUserShowNameByLoginName(Set loginNameSet) { + return new HashMap<>(1); + } + + /** + * 获取任务执行人是当前部门领导岗位的任务监听器。 + * 通常会在没有找到领导部门岗位Id的时候,为当前任务指定其他的指派人、候选人或候选组。 + * + * @return 任务监听器。 + */ + default Class getDeptPostLeaderListener() { + return DeptPostLeaderListener.class; + } + + /** + * 获取任务执行人是上级部门领导岗位的任务监听器。 + * 通常会在没有找到领导部门岗位Id的时候,为当前任务指定其他的指派人、候选人或候选组。 + * + * @return 任务监听器。 + */ + default Class getUpDeptPostLeaderListener() { + return UpDeptPostLeaderListener.class; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java new file mode 100644 index 00000000..d90cd432 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseFlowNotifyExtHelper.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.flow.util; + +import com.orangeforms.common.flow.vo.FlowTaskVo; +import com.orangeforms.common.flow.vo.FlowUserInfoVo; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 流程通知扩展帮助实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class BaseFlowNotifyExtHelper { + + /** + * 处理消息。 + * + * @param notifyType 通知类型,具体值可参考FlowUserTaskExtData中NOTIFY_TYPE开头的常量。 + * @param userInfoList 待通知的用户信息列表。 + */ + public void doNotify(String notifyType, List userInfoList, FlowTaskVo taskInfo) { + userInfoList.forEach(u -> log.info( + "The user [{}] of Task [{}] is notified by [{}].", u.getLoginName(), taskInfo.getTaskKey(), notifyType)); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java new file mode 100644 index 00000000..76b0e2cb --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/BaseOnlineBusinessDataExtHelper.java @@ -0,0 +1,51 @@ +package com.orangeforms.common.flow.util; + +import cn.hutool.core.lang.Assert; +import com.orangeforms.common.flow.base.service.BaseFlowOnlineService; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import lombok.extern.slf4j.Slf4j; + +/** + * 面向在线表单工作流的业务数据扩展帮助实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class BaseOnlineBusinessDataExtHelper { + + private BaseFlowOnlineService onlineBusinessService; + + /** + * 设置在线表单的业务处理服务。 + * + * @param onlineBusinessService 在线表单业务处理服务实现类。 + */ + public void setOnlineBusinessService(BaseFlowOnlineService onlineBusinessService) { + this.onlineBusinessService = onlineBusinessService; + } + + /** + * 更新在线表单主表数据的流程状态字段值。 + * + * @param workOrder 工单对象。 + */ + public void updateFlowStatus(FlowWorkOrder workOrder) { + Assert.notNull(workOrder.getOnlineTableId()); + if (this.onlineBusinessService != null && workOrder.getBusinessKey() != null) { + onlineBusinessService.updateFlowStatus(workOrder); + } + } + + /** + * 根据工单对象级联删除业务数据。 + * + * @param workOrder 工单对象。 + */ + public void deleteBusinessData(FlowWorkOrder workOrder) { + Assert.notNull(workOrder.getOnlineTableId()); + if (this.onlineBusinessService != null && workOrder.getBusinessKey() != null) { + onlineBusinessService.deleteBusinessData(workOrder); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java new file mode 100644 index 00000000..aa05956c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomChangeActivityStateBuilderImpl.java @@ -0,0 +1,29 @@ +package com.orangeforms.common.flow.util; + +import org.flowable.engine.impl.RuntimeServiceImpl; +import org.flowable.engine.impl.runtime.ChangeActivityStateBuilderImpl; +import org.flowable.engine.runtime.ChangeActivityStateBuilder; + +import java.util.List; + +/** + * 自定义修改活动状态构建器实现。主要用于支持多个源节点向多个目标节点跳转的功能。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class CustomChangeActivityStateBuilderImpl extends ChangeActivityStateBuilderImpl { + + public CustomChangeActivityStateBuilderImpl() { + super(); + } + + public CustomChangeActivityStateBuilderImpl(RuntimeServiceImpl runtimeService) { + super(runtimeService); + } + + public ChangeActivityStateBuilder moveActivityIdsToActivityIds(List activityIds, List moveToActivityIds) { + moveActivityIdList.add(new CustomMoveActivityIdContainer(activityIds, moveToActivityIds)); + return this; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java new file mode 100644 index 00000000..66fa2e7e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/CustomMoveActivityIdContainer.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.flow.util; + +import org.flowable.engine.impl.runtime.MoveActivityIdContainer; + +import java.util.List; + +/** + * 自定义移动任务Id的容器类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class CustomMoveActivityIdContainer extends MoveActivityIdContainer { + + public CustomMoveActivityIdContainer(String singleActivityId, String moveToActivityId) { + super(singleActivityId, moveToActivityId); + } + + public CustomMoveActivityIdContainer(List activityIds, List moveToActivityIds) { + super(activityIds.get(0), moveToActivityIds.get(0)); + this.activityIds = activityIds; + this.moveToActivityIds = moveToActivityIds; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java new file mode 100644 index 00000000..422e016a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowCustomExtFactory.java @@ -0,0 +1,67 @@ +package com.orangeforms.common.flow.util; + +import org.springframework.stereotype.Component; + +/** + * 工作流自定义扩展工厂类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class FlowCustomExtFactory { + + private BaseFlowIdentityExtHelper flowIdentityExtHelper; + + private BaseOnlineBusinessDataExtHelper onlineBusinessDataExtHelper = new BaseOnlineBusinessDataExtHelper(); + + private BaseFlowNotifyExtHelper flowNotifyExtHelper; + + /** + * 获取业务模块自行实现的用户身份相关的扩展帮助实现类。 + * + * @return 业务模块自行实现的用户身份相关的扩展帮助实现类。 + */ + public BaseFlowIdentityExtHelper getFlowIdentityExtHelper() { + return flowIdentityExtHelper; + } + + /** + * 注册业务模块自行实现的用户身份扩展帮助实现类。 + * + * @param helper 业务模块自行实现的用户身份扩展帮助实现类。 + */ + public void registerFlowIdentityExtHelper(BaseFlowIdentityExtHelper helper) { + this.flowIdentityExtHelper = helper; + } + + /** + * 获取有关在线表单业务数据的扩展帮助实现类。 + * + * @return 有关业务数据的扩展帮助实现类。 + */ + public BaseOnlineBusinessDataExtHelper getOnlineBusinessDataExtHelper() { + return onlineBusinessDataExtHelper; + } + + /** + * 注册流程通知扩展帮助实现类。 + * + * @param helper 流程通知扩展帮助实现类。 + */ + public void registerNotifyExtHelper(BaseFlowNotifyExtHelper helper) { + this.flowNotifyExtHelper = helper; + } + + /** + * 获取流程通知扩展帮助实现类。 + * + * @return 流程消息通知扩展帮助实现类。 + */ + public BaseFlowNotifyExtHelper getFlowNotifyExtHelper() { + if (this.flowNotifyExtHelper == null) { + this.flowNotifyExtHelper = new BaseFlowNotifyExtHelper(); + } + return flowNotifyExtHelper; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java new file mode 100644 index 00000000..3b3ebc8e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowOperationHelper.java @@ -0,0 +1,505 @@ +package com.orangeforms.common.flow.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.flow.constant.FlowApprovalType; +import com.orangeforms.common.flow.constant.FlowConstant; +import com.orangeforms.common.flow.constant.FlowTaskStatus; +import com.orangeforms.common.flow.dto.FlowTaskCommentDto; +import com.orangeforms.common.flow.dto.FlowWorkOrderDto; +import com.orangeforms.common.flow.model.FlowEntry; +import com.orangeforms.common.flow.model.FlowEntryPublish; +import com.orangeforms.common.flow.model.FlowWorkOrder; +import com.orangeforms.common.flow.model.constant.FlowEntryStatus; +import com.orangeforms.common.flow.object.FlowEntryExtensionData; +import com.orangeforms.common.flow.object.FlowRumtimeObject; +import com.orangeforms.common.flow.service.FlowApiService; +import com.orangeforms.common.flow.service.FlowEntryService; +import com.orangeforms.common.flow.service.FlowWorkOrderService; +import com.orangeforms.common.flow.vo.FlowWorkOrderVo; +import com.orangeforms.common.flow.vo.TaskInfoVo; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 工作流操作的通用帮助对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class FlowOperationHelper { + + @Autowired + private FlowEntryService flowEntryService; + @Autowired + private FlowApiService flowApiService; + @Autowired + private FlowWorkOrderService flowWorkOrderService; + @Autowired + private FlowCustomExtFactory flowCustomExtFactory; + + /** + * 验证并获取流程对象。 + * + * @param processDefinitionKey 流程引擎的流程定义标识。 + * @return 流程对象。 + */ + public ResponseResult verifyAndGetFlowEntry(String processDefinitionKey) { + String errorMessage; + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (flowEntry == null) { + errorMessage = "数据验证失败,该流程并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!flowEntry.getStatus().equals(FlowEntryStatus.PUBLISHED)) { + errorMessage = "数据验证失败,该流程尚未发布,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowEntryPublish flowEntryPublish = + flowEntryService.getFlowEntryPublishFromCache(flowEntry.getMainEntryPublishId()); + flowEntry.setMainFlowEntryPublish(flowEntryPublish); + return ResponseResult.success(flowEntry); + } + + /** + * 验证并获取流程发布对象。 + * + * @param processDefinitionKey 流程引擎的流程定义标识。 + * @return 流程对象。 + */ + public ResponseResult verifyAndGetFlowEntryPublish(String processDefinitionKey) { + // 1. 验证流程数据的合法性。 + ResponseResult flowEntryResult = this.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + // 2. 验证流程一个用户任务的合法性。 + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + ResponseResult taskInfoResult = this.verifyAndGetInitialTaskInfo(flowEntryPublish, false); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + return ResponseResult.success(flowEntryPublish); + } + + /** + * 工作流静态表单的参数验证工具方法。根据流程定义标识,获取关联的流程并对其进行合法性验证。 + * + * @param processDefinitionKey 流程定义标识。 + * @return 返回流程对象。 + */ + public ResponseResult verifyFullAndGetFlowEntry(String processDefinitionKey) { + String errorMessage; + // 验证流程管理数据状态的合法性。 + ResponseResult flowEntryResult = this.verifyAndGetFlowEntry(processDefinitionKey); + if (!flowEntryResult.isSuccess()) { + return ResponseResult.errorFrom(flowEntryResult); + } + // 验证流程一个用户任务的合法性。 + FlowEntryPublish flowEntryPublish = flowEntryResult.getData().getMainFlowEntryPublish(); + if (BooleanUtil.isFalse(flowEntryPublish.getActiveStatus())) { + errorMessage = "数据验证失败,当前流程发布对象已被挂起,不能启动新流程!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult taskInfoResult = + this.verifyAndGetInitialTaskInfo(flowEntryPublish, true); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + return flowEntryResult; + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的流程任务对象。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批对象。 + * @return 验证后的流程任务对象。 + */ + public ResponseResult verifySubmitAndGetTask( + String processInstanceId, String taskId, FlowTaskCommentDto flowTaskComment) { + // 验证流程任务的合法性。 + Task task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + ResponseResult taskInfoResult = this.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + CallResult assigneeVerifyResult = flowApiService.verifyAssigneeOrCandidateAndClaim(task); + if (!assigneeVerifyResult.isSuccess()) { + return ResponseResult.errorFrom(assigneeVerifyResult); + } + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + if (StrUtil.isBlank(instance.getBusinessKey())) { + return ResponseResult.success(task); + } + String errorMessage; + if (flowTaskComment != null + && StrUtil.equals(flowTaskComment.getApprovalType(), FlowApprovalType.TRANSFER) + && StrUtil.isBlank(flowTaskComment.getDelegateAssignee())) { + errorMessage = "数据验证失败,加签或转办任务指派人不能为空!!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(task); + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的流程任务和流程实例。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @param flowTaskComment 流程审批对象。 + * @param processDefinitionKey 流程定义标识。 + * @return 验证后的流程运行时常用对象。 + */ + public ResponseResult verifySubmitWithGetInstanceAndTask( + String processInstanceId, String taskId, FlowTaskCommentDto flowTaskComment, String processDefinitionKey) { + ResponseResult taskResult = this.verifySubmitAndGetTask(processInstanceId, taskId, flowTaskComment); + if (!taskResult.isSuccess()) { + return ResponseResult.errorFrom(taskResult); + } + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + if (!StrUtil.equals(instance.getProcessDefinitionKey(), processDefinitionKey)) { + String errorMessage = "数据验证失败,请求流程标识与流程实例不匹配,请核对!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + FlowRumtimeObject o = new FlowRumtimeObject(); + o.setTask(taskResult.getData()); + o.setInstance(instance); + return ResponseResult.success(o); + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的历史流程实例对象。 + * 仅当登录用户为任务的分配人时,才能通过验证。 + * + * @param processInstanceId 历史流程实例Id。 + * @param taskId 历史流程任务Id。 + * @return 验证后并返回的历史流程实例对象。 + */ + public ResponseResult verifyAndGetHistoricProcessInstance(String processInstanceId, String taskId) { + String errorMessage; + // 验证流程实例的合法性。 + HistoricProcessInstance instance = flowApiService.getHistoricProcessInstance(processInstanceId); + if (instance == null) { + errorMessage = "数据验证失败,指定的流程实例Id并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String loginName = TokenData.takeFromRequest().getLoginName(); + if (StrUtil.isBlank(taskId)) { + if (!StrUtil.equals(loginName, instance.getStartUserId()) + && !flowWorkOrderService.hasDataPermOnFlowWorkOrder(processInstanceId)) { + errorMessage = "数据验证失败,指定历史流程的发起人与当前用户不匹配,或者没有查看该工单详情的数据权限!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } else { + HistoricTaskInstance taskInstance = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (taskInstance == null) { + errorMessage = "数据验证失败,指定的任务Id并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(loginName, taskInstance.getAssignee()) + && !flowWorkOrderService.hasDataPermOnFlowWorkOrder(processInstanceId)) { + errorMessage = "数据验证失败,历史任务的指派人与当前用户不匹配,或者没有查看该工单详情的数据权限!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(instance); + } + + /** + * 工作流静态表单的参数验证工具方法。根据参数验证并获取指定的历史流程实例对象。 + * 仅当登录用户为任务的分配人时,才能通过验证。 + * + * @param processInstanceId 历史流程实例Id。 + * @param taskId 历史流程任务Id。 + * @param processDefinitionKey 流程定义标识。 + * @return 验证后并返回的历史流程实例对象。 + */ + public ResponseResult verifyAndGetHistoricProcessInstance( + String processInstanceId, String taskId, String processDefinitionKey) { + ResponseResult instanceResult = + this.verifyAndGetHistoricProcessInstance(processInstanceId, taskId); + if (!instanceResult.isSuccess()) { + return ResponseResult.errorFrom(instanceResult); + } + HistoricProcessInstance instance = instanceResult.getData(); + if (!StrUtil.equals(instance.getProcessDefinitionKey(), processDefinitionKey)) { + String errorMessage = "数据验证失败,请求流程标识与流程实例不匹配,请核对!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(instance); + } + + /** + * 验证并获取流程的实时任务信息。 + * + * @param task 流程引擎的任务对象。 + * @return 任务信息对象。 + */ + public ResponseResult verifyAndGetRuntimeTaskInfo(Task task) { + String errorMessage; + if (task == null) { + errorMessage = "数据验证失败,指定的任务Id不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowApiService.isAssigneeOrCandidate(task)) { + errorMessage = "数据验证失败,当前用户不是指派人也不是候选人之一!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (StrUtil.isBlank(task.getFormKey())) { + errorMessage = "数据验证失败,指定任务的formKey属性不存在,请重新修改流程图!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfo = JSON.parseObject(task.getFormKey(), TaskInfoVo.class); + taskInfo.setTaskKey(task.getTaskDefinitionKey()); + return ResponseResult.success(taskInfo); + } + + /** + * 验证并获取启动任务的对象信息。 + * + * @param flowEntryPublish 流程发布对象。 + * @param checkStarter 是否检查发起用户。 + * @return 第一个可执行的任务信息。 + */ + public ResponseResult verifyAndGetInitialTaskInfo( + FlowEntryPublish flowEntryPublish, boolean checkStarter) { + String errorMessage; + if (StrUtil.isBlank(flowEntryPublish.getInitTaskInfo())) { + errorMessage = "数据验证失败,当前流程发布的数据中,没有包含初始任务信息!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TaskInfoVo taskInfo = JSON.parseObject(flowEntryPublish.getInitTaskInfo(), TaskInfoVo.class); + if (checkStarter) { + String loginName = TokenData.takeFromRequest().getLoginName(); + if (!StrUtil.equalsAny(taskInfo.getAssignee(), loginName, FlowConstant.START_USER_NAME_VAR)) { + errorMessage = "数据验证失败,该工作流第一个用户任务的指派人并非当前用户,不能执行该操作!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(taskInfo); + } + + /** + * 判断当前用户是否有当前流程实例的数据上传或下载权限。 + * 如果taskId为空,则验证当前用户是否为当前流程实例的发起人,否则判断是否为当前任务的指派人或候选人。 + * + * @param processInstanceId 流程实例Id。 + * @param taskId 流程任务Id。 + * @return 验证结果。 + */ + public ResponseResult verifyUploadOrDownloadPermission(String processInstanceId, String taskId) { + if (flowApiService.isProcessInstanceStarter(processInstanceId)) { + return ResponseResult.success(); + } + String errorMessage; + if (StrUtil.isBlank(taskId)) { + errorMessage = "数据验证失败,当前用户没有权限下载!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + TaskInfo task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + if (task == null) { + task = flowApiService.getHistoricTaskInstance(processInstanceId, taskId); + if (task == null) { + errorMessage = "数据验证失败,指定任务Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + } + if (!flowApiService.isAssigneeOrCandidate(task)) { + errorMessage = "数据验证失败,当前用户并非指派人或候选人,因此没有权限下载!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 根据已有的过滤对象,补充添加缺省过滤条件。如流程标识、创建用户等。 + * + * @param filterDto 工单过滤对象。 + * @param processDefinitionKey 流程标识。 + * @return 创建并转换后的流程工单过滤对象。 + */ + public FlowWorkOrder makeWorkOrderFilter(FlowWorkOrderDto filterDto, String processDefinitionKey) { + FlowWorkOrder filter = MyModelUtil.copyTo(filterDto, FlowWorkOrder.class); + if (filter == null) { + filter = new FlowWorkOrder(); + } + filter.setProcessDefinitionKey(processDefinitionKey); + // 下面的方法会帮助构建工单的数据权限过滤条件,和业务希望相比,如果当前系统没有支持数据权限, + // 用户则只能看到自己发起的工单,否则按照数据权限过滤。然而需要特殊处理的是,如果用户的数据 + // 权限中,没有包含能看自己,这里也需要自动给加上。 + BaseFlowIdentityExtHelper flowIdentityExtHelper = flowCustomExtFactory.getFlowIdentityExtHelper(); + if (BooleanUtil.isFalse(flowIdentityExtHelper.supprtDataPerm())) { + filter.setCreateUserId(TokenData.takeFromRequest().getUserId()); + } + return filter; + } + + /** + * 组装工作流工单列表中的流程任务数据。 + * + * @param flowWorkOrderVoList 工作流工单列表。 + */ + public void buildWorkOrderTaskInfo(List flowWorkOrderVoList) { + if (CollUtil.isEmpty(flowWorkOrderVoList)) { + return; + } + Set definitionIdSet = + flowWorkOrderVoList.stream().map(FlowWorkOrderVo::getProcessDefinitionId).collect(Collectors.toSet()); + List flowEntryPublishList = flowEntryService.getFlowEntryPublishList(definitionIdSet); + Map flowEntryPublishMap = + flowEntryPublishList.stream().collect(Collectors.toMap(FlowEntryPublish::getProcessDefinitionId, c -> c)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + FlowEntryPublish flowEntryPublish = flowEntryPublishMap.get(flowWorkOrderVo.getProcessDefinitionId()); + flowWorkOrderVo.setInitTaskInfo(flowEntryPublish.getInitTaskInfo()); + } + List unfinishedProcessInstanceIds = flowWorkOrderVoList.stream() + .filter(c -> !c.getFlowStatus().equals(FlowTaskStatus.FINISHED)) + .map(FlowWorkOrderVo::getProcessInstanceId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(unfinishedProcessInstanceIds)) { + return; + } + List taskList = flowApiService.getTaskListByProcessInstanceIds(unfinishedProcessInstanceIds); + Map> taskMap = + taskList.stream().collect(Collectors.groupingBy(Task::getProcessInstanceId)); + for (FlowWorkOrderVo flowWorkOrderVo : flowWorkOrderVoList) { + List instanceTaskList = taskMap.get(flowWorkOrderVo.getProcessInstanceId()); + if (instanceTaskList == null) { + continue; + } + JSONArray taskArray = new JSONArray(); + for (Task task : instanceTaskList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("taskId", task.getId()); + jsonObject.put("taskName", task.getName()); + jsonObject.put("taskKey", task.getTaskDefinitionKey()); + jsonObject.put("assignee", task.getAssignee()); + taskArray.add(jsonObject); + } + flowWorkOrderVo.setRuntimeTaskInfoList(taskArray); + } + } + + /** + * 组装工作流工单中的业务数据。 + * + * @param workOrderVoList 工单列表。 + * @param dataList 业务数据列表。 + * @param idGetter 获取业务对象主键字段的返回方法。 + * @param 业务主对象类型。 + * @param 业务主对象的主键字段类型。 + */ + public void buildWorkOrderBusinessData( + List workOrderVoList, List dataList, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return; + } + Map dataMap = dataList.stream().collect(Collectors.toMap(idGetter, c -> c)); + K id = idGetter.apply(dataList.get(0)); + for (FlowWorkOrderVo flowWorkOrderVo : workOrderVoList) { + if (StrUtil.isBlank(flowWorkOrderVo.getBusinessKey())) { + continue; + } + Object dataId = flowWorkOrderVo.getBusinessKey(); + if (id instanceof Long) { + dataId = Long.valueOf(flowWorkOrderVo.getBusinessKey()); + } else if (id instanceof Integer) { + dataId = Integer.valueOf(flowWorkOrderVo.getBusinessKey()); + } + T data = dataMap.get(dataId); + if (data != null) { + flowWorkOrderVo.setMasterData(BeanUtil.beanToMap(data)); + } + } + } + + /** + * 验证并根据流程实例Id获取处于草稿状态的流程工单。 + * + * @param processDefinitionKey 流程定义标识。 + * @param processInstanceId 流程实例Id。 + * @return 流程工单。 + */ + public ResponseResult verifyAndGetFlowWorkOrderWithDraft( + String processDefinitionKey, String processInstanceId) { + String errorMessage; + FlowWorkOrder flowWorkOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); + if (flowWorkOrder == null) { + errorMessage = "数据验证失败,流程实例关联的工单不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowWorkOrder.getFlowStatus().equals(FlowTaskStatus.DRAFT)) { + errorMessage = "数据验证失败,当前流程工单并不处于草稿保存状态!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!flowWorkOrder.getCreateUserId().equals(TokenData.takeFromRequest().getUserId())) { + errorMessage = "数据验证失败,草稿数据保存用户与当前用户不一致!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (processDefinitionKey != null && !flowWorkOrder.getProcessDefinitionKey().equals(processDefinitionKey)) { + errorMessage = "数据验证失败,流程实例和流程定义标识不匹配!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(flowWorkOrder); + } + + /** + * 根据流程定义的扩展数据中的审批状态字典列表数据,组装工单列表中,每个工单对象的审批状态字典数据。 + * @param processDefinitionKey 流程定义标识。 + * @param workOrderVoList 待组装的工单列表。 + */ + public void buildWorkOrderApprovalStatus(String processDefinitionKey, List workOrderVoList) { + FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey); + if (StrUtil.isBlank(flowEntry.getExtensionData())) { + return; + } + FlowEntryExtensionData extensionData = + JSON.parseObject(flowEntry.getExtensionData(), FlowEntryExtensionData.class); + if (CollUtil.isEmpty(extensionData.getApprovalStatusDict())) { + return; + } + Map dictMap = new HashMap<>(extensionData.getApprovalStatusDict().size()); + for (Map m : extensionData.getApprovalStatusDict()) { + dictMap.put(Integer.valueOf(m.get("id")), m.get("name")); + } + for (FlowWorkOrderVo workOrderVo : workOrderVoList) { + if (workOrderVo.getLatestApprovalStatus() != null) { + String name = dictMap.get(workOrderVo.getLatestApprovalStatus()); + if (name != null) { + Map lastestApprovalStatusDictMap = MapUtil.newHashMap(); + lastestApprovalStatusDictMap.put("id", workOrderVo.getLatestApprovalStatus()); + lastestApprovalStatusDictMap.put("name", name); + workOrderVo.setLatestApprovalStatusDictMap(lastestApprovalStatusDictMap); + } + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java new file mode 100644 index 00000000..b95cd08e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/util/FlowRedisKeyUtil.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.flow.util; + +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.object.TokenData; + +/** + * 工作流 Redis 键生成工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class FlowRedisKeyUtil { + + /** + * 计算流程对象缓存在Redis中的键值。 + * + * @param processDefinitionKey 流程标识。 + * @return 流程对象缓存在Redis中的键值。 + */ + public static String makeFlowEntryKey(String processDefinitionKey) { + String prefix = "FLOW_ENTRY:"; + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData == null) { + return prefix + processDefinitionKey; + } + String appCode = tokenData.getAppCode(); + if (StrUtil.isBlank(appCode)) { + Long tenantId = tokenData.getTenantId(); + if (tenantId == null) { + return prefix + processDefinitionKey; + } + return prefix + tenantId.toString() + ":" + processDefinitionKey; + } + return prefix + appCode + ":" + processDefinitionKey; + } + + /** + * 流程发布对象缓存在Redis中的键值。 + * + * @param flowEntryPublishId 流程发布主键Id。 + * @return 流程发布对象缓存在Redis中的键值。 + */ + public static String makeFlowEntryPublishKey(Long flowEntryPublishId) { + return "FLOW_ENTRY_PUBLISH:" + flowEntryPublishId; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FlowRedisKeyUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java new file mode 100644 index 00000000..56894a81 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowCategoryVo.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程分类的Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程分类的Vo对象") +@Data +public class FlowCategoryVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long categoryId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码") + private String appCode; + + /** + * 显示名称。 + */ + @Schema(description = "显示名称") + private String name; + + /** + * 分类编码。 + */ + @Schema(description = "分类编码") + private String code; + + /** + * 实现顺序。 + */ + @Schema(description = "实现顺序") + private Integer showOrder; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java new file mode 100644 index 00000000..53c802fa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryPublishVo.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程发布信息的Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程发布信息的Vo对象") +@Data +public class FlowEntryPublishVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long entryPublishId; + + /** + * 发布版本。 + */ + @Schema(description = "发布版本") + private Integer publishVersion; + + /** + * 流程引擎中的流程定义Id。 + */ + @Schema(description = "流程引擎中的流程定义Id") + private String processDefinitionId; + + /** + * 激活状态。 + */ + @Schema(description = "激活状态") + private Boolean activeStatus; + + /** + * 是否为主版本。 + */ + @Schema(description = "是否为主版本") + private Boolean mainVersion; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 发布时间。 + */ + @Schema(description = "发布时间") + private Date publishTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java new file mode 100644 index 00000000..68ef4d33 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVariableVo.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程变量Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程变量Vo对象") +@Data +public class FlowEntryVariableVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long variableId; + + /** + * 流程Id。 + */ + @Schema(description = "流程Id") + private Long entryId; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + private String variableName; + + /** + * 显示名。 + */ + @Schema(description = "显示名") + private String showName; + + /** + * 变量类型。 + */ + @Schema(description = "变量类型") + private Integer variableType; + + /** + * 绑定数据源Id。 + */ + @Schema(description = "绑定数据源Id") + private Long bindDatasourceId; + + /** + * 绑定数据源关联Id。 + */ + @Schema(description = "绑定数据源关联Id") + private Long bindRelationId; + + /** + * 绑定字段Id。 + */ + @Schema(description = "绑定字段Id") + private Long bindColumnId; + + /** + * 是否内置。 + */ + @Schema(description = "是否内置") + private Boolean builtin; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java new file mode 100644 index 00000000..b9cdc945 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowEntryVo.java @@ -0,0 +1,157 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 流程的Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程的Vo对象") +@Data +public class FlowEntryVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long entryId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码") + private String appCode; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程标识Key。 + */ + @Schema(description = "流程标识Key") + private String processDefinitionKey; + + /** + * 流程分类。 + */ + @Schema(description = "流程分类") + private Long categoryId; + + /** + * 工作流部署的发布主版本Id。 + */ + @Schema(description = "工作流部署的发布主版本Id") + private Long mainEntryPublishId; + + /** + * 最新发布时间。 + */ + @Schema(description = "最新发布时间") + private Date latestPublishTime; + + /** + * 流程状态。 + */ + @Schema(description = "流程状态") + private Integer status; + + /** + * 流程定义的xml。 + */ + @Schema(description = "流程定义的xml") + private String bpmnXml; + + /** + * 流程图类型。0: 普通流程图,1: 钉钉风格的流程图。 + */ + @Schema(description = "流程图类型。0: 普通流程图,1: 钉钉风格的流程图") + private Integer diagramType; + + /** + * 绑定表单类型。 + */ + @Schema(description = "绑定表单类型") + private Integer bindFormType; + + /** + * 在线表单的页面Id。 + */ + @Schema(description = "在线表单的页面Id") + private Long pageId; + + /** + * 在线表单Id。 + */ + @Schema(description = "在线表单Id") + private Long defaultFormId; + + /** + * 在线表单的缺省路由名称。 + */ + @Schema(description = "在线表单的缺省路由名称") + private String defaultRouterName; + + /** + * 工单表编码字段的编码规则,如果为空则不计算工单编码。 + */ + @Schema(description = "工单表编码字段的编码规则") + private String encodedRule; + + /** + * 流程的自定义扩展数据(JSON格式)。 + */ + @Schema(description = "流程的自定义扩展数据") + private String extensionData; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * categoryId 的一对一关联数据对象,数据对应类型为FlowCategoryVo。 + */ + @Schema(description = "categoryId 的一对一关联数据对象") + private Map flowCategory; + + /** + * mainEntryPublishId 的一对一关联数据对象,数据对应类型为FlowEntryPublishVo。 + */ + @Schema(description = "mainEntryPublishId 的一对一关联数据对象") + private Map mainFlowEntryPublish; + + /** + * 关联的在线表单列表。 + */ + @Schema(description = "关联的在线表单列表") + private List> formList; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java new file mode 100644 index 00000000..8d7d104b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowMessageVo.java @@ -0,0 +1,137 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 工作流通知消息Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流通知消息Vo对象") +@Data +public class FlowMessageVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long messageId; + + /** + * 消息类型。 + */ + @Schema(description = "消息类型") + private Integer messageType; + + /** + * 消息内容。 + */ + @Schema(description = "消息内容") + private String messageContent; + + /** + * 催办次数。 + */ + @Schema(description = "催办次数") + private Integer remindCount; + + /** + * 工单Id。 + */ + @Schema(description = "工单Id") + private Long workOrderId; + + /** + * 流程定义Id。 + */ + @Schema(description = "流程定义Id") + private String processDefinitionId; + + /** + * 流程定义标识。 + */ + @Schema(description = "流程定义标识") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 流程实例发起者。 + */ + @Schema(description = "流程实例发起者") + private String processInstanceInitiator; + + /** + * 流程任务Id。 + */ + @Schema(description = "流程任务Id") + private String taskId; + + /** + * 流程任务定义标识。 + */ + @Schema(description = "流程任务定义标识") + private String taskDefinitionKey; + + /** + * 流程任务名称。 + */ + @Schema(description = "流程任务名称") + private String taskName; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date taskStartTime; + + /** + * 业务数据快照。 + */ + @Schema(description = "业务数据快照") + private String businessDataShot; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 创建者显示名。 + */ + @Schema(description = "创建者显示名") + private String createUsername; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java new file mode 100644 index 00000000..c8328b34 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskCommentVo.java @@ -0,0 +1,113 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * FlowTaskCommentVO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "FlowTaskCommentVO对象") +@Data +public class FlowTaskCommentVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long id; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 任务Id。 + */ + @Schema(description = "任务Id") + private String taskId; + + /** + * 任务标识。 + */ + @Schema(description = "任务标识") + private String taskKey; + + /** + * 任务名称。 + */ + @Schema(description = "任务名称") + private String taskName; + + /** + * 任务的执行Id。 + */ + @Schema(description = "任务的执行Id") + private String executionId; + + /** + * 会签任务的执行Id。 + */ + @Schema(description = "会签任务的执行Id") + private String multiInstanceExecId; + + /** + * 审批类型。 + */ + @Schema(description = "审批类型") + private String approvalType; + + /** + * 批注内容。 + */ + @Schema(description = "批注内容") + private String taskComment; + + /** + * 委托指定人,比如加签、转办等。 + */ + @Schema(description = "委托指定人,比如加签、转办等") + private String delegateAssignee; + + /** + * 自定义数据。开发者可自行扩展,推荐使用JSON格式数据。 + */ + @Schema(description = "自定义数据") + private String customBusinessData; + + /** + * 审批人头像。 + */ + @Schema(description = "审批人头像") + private String headImageUrl; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * 创建者登录名。 + */ + @Schema(description = "创建者登录名") + private String createLoginName; + + /** + * 创建者显示名。 + */ + @Schema(description = "创建者显示名") + private String createUsername; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java new file mode 100644 index 00000000..35e4c367 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowTaskVo.java @@ -0,0 +1,125 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程任务Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务Vo对象") +@Data +public class FlowTaskVo { + + /** + * 流程任务Id。 + */ + @Schema(description = "流程任务Id") + private String taskId; + + /** + * 流程任务名称。 + */ + @Schema(description = "流程任务名称") + private String taskName; + + /** + * 流程任务标识。 + */ + @Schema(description = "流程任务标识") + private String taskKey; + + /** + * 任务的表单信息。 + */ + @Schema(description = "任务的表单信息") + private String taskFormKey; + + /** + * 待办任务开始时间。 + */ + @Schema(description = "待办任务开始时间") + private Date taskStartTime; + + /** + * 流程Id。 + */ + @Schema(description = "流程Id") + private Long entryId; + + /** + * 流程定义Id。 + */ + @Schema(description = "流程定义Id") + private String processDefinitionId; + + /** + * 流程定义名称。 + */ + @Schema(description = "流程定义名称") + private String processDefinitionName; + + /** + * 流程定义标识。 + */ + @Schema(description = "流程定义标识") + private String processDefinitionKey; + + /** + * 流程定义版本。 + */ + @Schema(description = "流程定义版本") + private Integer processDefinitionVersion; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 流程实例发起人。 + */ + @Schema(description = "流程实例发起人") + private String processInstanceInitiator; + + /** + * 流程实例发起人显示名。 + */ + @Schema(description = "流程实例发起人显示名") + private String showName; + + /** + * 用户头像信息。 + */ + @Schema(description = "用户头像信息") + private String headImageUrl; + + /** + * 流程实例创建时间。 + */ + @Schema(description = "流程实例创建时间") + private Date processInstanceStartTime; + + /** + * 流程实例主表业务数据主键。 + */ + @Schema(description = "流程实例主表业务数据主键") + private String businessKey; + + /** + * 工单编码。 + */ + @Schema(description = "工单编码") + private String workOrderCode; + + /** + * 是否为草稿状态。 + */ + @Schema(description = "是否为草稿状态") + private Boolean isDraft; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java new file mode 100644 index 00000000..2ceca1fa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowUserInfoVo.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 流程任务的用户信息。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务的用户信息") +@Data +public class FlowUserInfoVo { + + /** + * 用户Id。 + */ + @Schema(description = "用户Id") + private Long userId; + + /** + * 用户部门Id。 + */ + @Schema(description = "用户部门Id") + private Long deptId; + + /** + * 登录用户名。 + */ + @Schema(description = "登录用户名") + private String loginName; + + /** + * 用户显示名称。 + */ + @Schema(description = "用户显示名称") + private String showName; + + /** + * 用户头像的Url。 + */ + @Schema(description = "用户头像的Url") + private String headImageUrl; + + /** + * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。 + */ + @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)") + private Integer userType; + + /** + * 用户状态(0: 正常 1: 锁定)。 + */ + @Schema(description = "用户状态(0: 正常 1: 锁定)") + private Integer userStatus; + + /** + * 用户邮箱。 + */ + @Schema(description = "用户邮箱") + private String email; + + /** + * 用户手机。 + */ + @Schema(description = "用户手机") + private String mobile; + + /** + * 最后审批时间。 + */ + @Schema(description = "最后审批时间") + private Date lastApprovalTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java new file mode 100644 index 00000000..3122ed8f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/FlowWorkOrderVo.java @@ -0,0 +1,158 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.alibaba.fastjson.JSONArray; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 工作流工单VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "工作流工单Vo对象") +@Data +public class FlowWorkOrderVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long workOrderId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码") + private String appCode; + + /** + * 工单编码字段。 + */ + @Schema(description = "工单编码字段") + private String workOrderCode; + + /** + * 流程定义标识。 + */ + @Schema(description = "流程定义标识") + private String processDefinitionKey; + + /** + * 流程名称。 + */ + @Schema(description = "流程名称") + private String processDefinitionName; + + /** + * 流程引擎的定义Id。 + */ + @Schema(description = "流程引擎的定义Id") + private String processDefinitionId; + + /** + * 流程实例Id。 + */ + @Schema(description = "流程实例Id") + private String processInstanceId; + + /** + * 在线表单的主表Id。 + */ + @Schema(description = "在线表单的主表Id") + private Long onlineTableId; + + /** + * 业务主键值。 + */ + @Schema(description = "业务主键值") + private String businessKey; + + /** + * 最近的审批状态。 + */ + @Schema(description = "最近的审批状态") + private Integer latestApprovalStatus; + + /** + * 流程状态。参考FlowTaskStatus常量值对象。 + */ + @Schema(description = "流程状态") + private Integer flowStatus; + + /** + * 提交用户登录名称。 + */ + @Schema(description = "提交用户登录名称") + private String submitUsername; + + /** + * 提交用户所在部门Id。 + */ + @Schema(description = "提交用户所在部门Id") + private Long deptId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者Id。 + */ + @Schema(description = "更新者Id") + private Long updateUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者Id。 + */ + @Schema(description = "创建者Id") + private Long createUserId; + + /** + * latestApprovalStatus 关联的字典数据。 + */ + @Schema(description = "latestApprovalStatus 常量字典关联数据") + private Map latestApprovalStatusDictMap; + + /** + * flowStatus 常量字典关联数据。 + */ + @Schema(description = "flowStatus 常量字典关联数据") + private Map flowStatusDictMap; + + /** + * 用户的显示名。 + */ + @Schema(description = "用户的显示名") + private String userShowName; + + /** + * FlowEntryPublish对象中的同名字段。 + */ + @Schema(description = "FlowEntryPublish对象中的同名字段") + private String initTaskInfo; + + /** + * 当前实例的运行时任务列表。 + * 正常情况下只有一个,在并行网关下可能存在多个。 + */ + @Schema(description = "实例的运行时任务列表") + private JSONArray runtimeTaskInfoList; + + /** + * 业务主表数据。 + */ + @Schema(description = "业务主表数据") + private Map masterData; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java new file mode 100644 index 00000000..2d4f981a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/java/com/orangeforms/common/flow/vo/TaskInfoVo.java @@ -0,0 +1,85 @@ +package com.orangeforms.common.flow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +import java.util.List; + +/** + * 流程任务信息Vo对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "流程任务信息Vo对象") +@Data +public class TaskInfoVo { + + /** + * 流程节点任务类型。具体值可参考FlowTaskType常量值。 + */ + @Schema(description = "流程节点任务类型") + private Integer taskType; + + /** + * 指定人。 + */ + @Schema(description = "指定人") + private String assignee; + + /** + * 任务标识。 + */ + @Schema(description = "任务标识") + private String taskKey; + + /** + * 是否分配给当前登录用户的标记。 + * 当该值为true时,登录用户启动流程时,就自动完成了第一个用户任务。 + */ + @Schema(description = "是否分配给当前登录用户的标记") + private Boolean assignedMe; + + /** + * 动态表单Id。 + */ + @Schema(description = "动态表单Id") + private Long formId; + + /** + * PC端静态表单路由。 + */ + @Schema(description = "PC端静态表单路由") + private String routerName; + + /** + * 移动端静态表单路由。 + */ + @Schema(description = "移动端静态表单路由") + private String mobileRouterName; + + /** + * 候选组类型。 + */ + @Schema(description = "候选组类型") + private String groupType; + + /** + * 只读标记。 + */ + @Schema(description = "只读标记") + private Boolean readOnly; + + /** + * 前端所需的操作列表。 + */ + @Schema(description = "前端所需的操作列表") + List operationList; + + /** + * 任务节点的自定义变量列表。 + */ + @Schema(description = "任务节点的自定义变量列表") + List variableList; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator new file mode 100644 index 00000000..eda90b8a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/services/org.flowable.common.engine.impl.EngineConfigurator @@ -0,0 +1 @@ +com.orangeforms.common.flow.config.CustomEngineConfigurator \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..8c6f8611 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-flow/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.flow.config.FlowAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-log/pom.xml new file mode 100644 index 00000000..4f39b309 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-log + 1.0.0 + common-log + jar + + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java new file mode 100644 index 00000000..00bbe1f6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/IgnoreResponseLog.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.log.annotation; + +import java.lang.annotation.*; + +/** + * 忽略接口应答数据记录日志的注解。该注解会被OperationLogAspect处理。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface IgnoreResponseLog { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java new file mode 100644 index 00000000..32f6b591 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/annotation/OperationLog.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.log.annotation; + +import com.orangeforms.common.log.model.constant.SysOperationLogType; + +import java.lang.annotation.*; + +/** + * 操作日志记录注解。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OperationLog { + + /** + * 描述。 + */ + String description() default ""; + + /** + * 操作类型。 + */ + int type() default SysOperationLogType.OTHER; + + /** + * 是否保存应答结果。 + * 对于类似导出和文件下载之类的接口,该参与应该设置为false。 + */ + boolean saveResponse() default true; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java new file mode 100644 index 00000000..b71c5df0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/aop/OperationLogAspect.java @@ -0,0 +1,265 @@ +package com.orangeforms.common.log.aop; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.ContextUtil; +import com.orangeforms.common.core.util.IpUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.log.annotation.IgnoreResponseLog; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.config.OperationLogProperties; +import com.orangeforms.common.log.model.SysOperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.log.service.SysOperationLogService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 操作日志记录处理AOP对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class OperationLogAspect { + + @Value("${spring.application.name}") + private String serviceName; + @Autowired + private SysOperationLogService operationLogService; + @Autowired + private OperationLogProperties properties; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 错误信息、请求参数和应答结果字符串的最大长度。 + */ + private static final int MAX_LENGTH = 2000; + + /** + * 所有controller方法。 + */ + @Pointcut("execution(public * com.orangeforms..controller..*(..))") + public void operationLogPointCut() { + // 空注释,避免sonar警告 + } + + @Around("operationLogPointCut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 计时。 + long start = System.currentTimeMillis(); + HttpServletRequest request = ContextUtil.getHttpRequest(); + HttpServletResponse response = ContextUtil.getHttpResponse(); + String traceId = this.getTraceId(request); + request.setAttribute(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + // 将流水号通过应答头返回给前端,便于问题精确定位。 + response.setHeader(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + TokenData tokenData = TokenData.takeFromRequest(); + // 为日志框架设定变量,使日志可以输出更多有价值的信息。 + if (tokenData != null) { + MDC.put("sessionId", tokenData.getSessionId()); + MDC.put("userId", tokenData.getUserId().toString()); + } + String[] parameterNames = this.getParameterNames(joinPoint); + Object[] args = joinPoint.getArgs(); + JSONObject jsonArgs = new JSONObject(); + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (this.isNormalArgs(arg)) { + String parameterName = parameterNames[i]; + jsonArgs.put(parameterName, arg); + } + } + String params = jsonArgs.toJSONString(); + SysOperationLog operationLog = null; + OperationLog operationLogAnnotation = null; + boolean saveOperationLog = properties.isEnabled(); + if (saveOperationLog) { + operationLogAnnotation = getMethodAnnotation(joinPoint, OperationLog.class); + saveOperationLog = (operationLogAnnotation != null); + } + if (saveOperationLog) { + operationLog = this.buildSysOperationLog(operationLogAnnotation, joinPoint, params, traceId, tokenData); + } + Object result; + log.info("开始请求,url={}, reqData={}", request.getRequestURI(), params); + try { + // 调用原来的方法 + result = joinPoint.proceed(); + String respData = result == null ? "null" : JSON.toJSONString(result); + Long elapse = System.currentTimeMillis() - start; + if (saveOperationLog) { + this.operationLogPostProcess(operationLogAnnotation, respData, operationLog, result); + } + if (elapse > properties.getSlowLogMs()) { + log.warn("耗时较长的请求完成警告, url={},elapse={}ms reqData={} respData={}", + request.getRequestURI(), elapse, params, respData); + } + if (this.getMethodAnnotation(joinPoint, IgnoreResponseLog.class) == null) { + log.info("请求完成, url={},elapse={}ms, respData={}", request.getRequestURI(), elapse, respData); + } + } catch (Exception e) { + if (saveOperationLog) { + operationLog.setSuccess(false); + operationLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, MAX_LENGTH)); + } + log.error("请求报错,url={}, reqData={}, error={}", request.getRequestURI(), params, e.getMessage()); + throw e; + } finally { + if (saveOperationLog) { + operationLog.setElapse(System.currentTimeMillis() - start); + operationLogService.saveNewAsync(operationLog); + } + MDC.remove(ApplicationConstant.HTTP_HEADER_TRACE_ID); + if (tokenData != null) { + MDC.remove("sessionId"); + MDC.remove("userId"); + } + } + return result; + } + + private SysOperationLog buildSysOperationLog( + OperationLog operationLogAnnotation, + ProceedingJoinPoint joinPoint, + String params, + String traceId, + TokenData tokenData) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + SysOperationLog operationLog = new SysOperationLog(); + operationLog.setLogId(idGenerator.nextLongId()); + operationLog.setTraceId(traceId); + operationLog.setDescription(operationLogAnnotation.description()); + operationLog.setOperationType(operationLogAnnotation.type()); + operationLog.setServiceName(this.serviceName); + operationLog.setApiClass(joinPoint.getTarget().getClass().getName()); + operationLog.setApiMethod(operationLog.getApiClass() + "." + joinPoint.getSignature().getName()); + operationLog.setRequestMethod(request.getMethod()); + operationLog.setRequestUrl(request.getRequestURI()); + if (tokenData != null) { + operationLog.setRequestIp(tokenData.getLoginIp()); + } else { + operationLog.setRequestIp(IpUtil.getRemoteIpAddress(request)); + } + operationLog.setOperationTime(new Date()); + if (params != null) { + if (params.length() <= MAX_LENGTH) { + operationLog.setRequestArguments(params); + } else { + operationLog.setRequestArguments(StringUtils.substring(params, 0, MAX_LENGTH)); + } + } + if (tokenData != null) { + // 对于非多租户系统,该值为空可以忽略。 + operationLog.setTenantId(tokenData.getTenantId()); + operationLog.setSessionId(tokenData.getSessionId()); + operationLog.setOperatorId(tokenData.getUserId()); + operationLog.setOperatorName(tokenData.getLoginName()); + } + return operationLog; + } + + private void operationLogPostProcess( + OperationLog operationLogAnnotation, String respData, SysOperationLog operationLog, Object result) { + if (operationLogAnnotation.saveResponse()) { + if (respData.length() <= MAX_LENGTH) { + operationLog.setResponseResult(respData); + } else { + operationLog.setResponseResult(StringUtils.substring(respData, 0, MAX_LENGTH)); + } + } + // 处理大部分返回ResponseResult的接口。 + if (!(result instanceof ResponseResult)) { + if (ContextUtil.hasRequestContext()) { + operationLog.setSuccess(ContextUtil.getHttpResponse().getStatus() == HttpServletResponse.SC_OK); + } + return; + } + ResponseResult responseResult = (ResponseResult) result; + operationLog.setSuccess(responseResult.isSuccess()); + if (!responseResult.isSuccess()) { + operationLog.setErrorMsg(responseResult.getErrorMessage()); + } + if (operationLog.getOperationType().equals(SysOperationLogType.LOGIN)) { + // 对于登录操作,由于在调用登录方法之前,没有可用的TokenData。 + // 因此如果登录成功,可再次通过TokenData.takeFromRequest()获取TokenData。 + if (BooleanUtil.isTrue(operationLog.getSuccess())) { + // 这里为了保证LoginController.doLogin方法,一定将TokenData存入Request.Attribute之中, + // 我们将不做空值判断,一旦出错,开发者可在调试时立刻发现异常,并根据这里的注释进行修复。 + TokenData tokenData = TokenData.takeFromRequest(); + // 对于非多租户系统,为了保证代码一致性,仍可保留对tenantId的赋值代码。 + operationLog.setTenantId(tokenData.getTenantId()); + operationLog.setSessionId(tokenData.getSessionId()); + operationLog.setOperatorId(tokenData.getUserId()); + operationLog.setOperatorName(tokenData.getLoginName()); + } else { + HttpServletRequest request = ContextUtil.getHttpRequest(); + // 登录操作需要特殊处理,无论是登录成功还是失败,都要记录operator_name字段。 + operationLog.setOperatorName(request.getParameter("loginName")); + } + } + } + + private String[] getParameterNames(ProceedingJoinPoint joinPoint) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + return methodSignature.getParameterNames(); + } + + private T getMethodAnnotation(JoinPoint joinPoint, Class annotationClazz) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method method = methodSignature.getMethod(); + return method.getAnnotation(annotationClazz); + } + + private String getTraceId(HttpServletRequest request) { + // 获取请求流水号。 + // 对于微服务系统,为了保证traceId在全调用链的唯一性,因此在网关的过滤器中创建了该值。 + String traceId = request.getHeader(ApplicationConstant.HTTP_HEADER_TRACE_ID); + if (StringUtils.isBlank(traceId)) { + traceId = MyCommonUtil.generateUuid(); + } + return traceId; + } + + private boolean isNormalArgs(Object o) { + if (o instanceof List) { + List list = (List) o; + if (CollUtil.isNotEmpty(list)) { + return !(list.get(0) instanceof MultipartFile); + } + } + return !(o instanceof HttpServletRequest) + && !(o instanceof HttpServletResponse) + && !(o instanceof MultipartFile); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java new file mode 100644 index 00000000..54444158 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/CommonLogAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.log.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-log模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({OperationLogProperties.class}) +public class CommonLogAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java new file mode 100644 index 00000000..cd8c95d6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/config/OperationLogProperties.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.log.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 操作日志的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-log.operation-log") +public class OperationLogProperties { + + /** + * 是否采集操作日志。 + */ + private boolean enabled = true; + /** + * 接口调用的毫秒数大于该值后,将输出慢日志警告。 + */ + private long slowLogMs = 50000; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java new file mode 100644 index 00000000..63e5ec4c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/SysOperationLogMapper.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.log.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.log.model.SysOperationLog; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 系统操作日志对应的数据访问对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysOperationLogMapper extends BaseDaoMapper { + + /** + * 批量插入。 + * + * @param operationLogList 操作日志列表。 + */ + void insertList(List operationLogList); + + /** + * 根据过滤条件和排序规则,查询操作日志。 + * + * @param sysOperationLogFilter 操作日志的过滤对象。 + * @param orderBy 排序规则。 + * @return 查询列表。 + */ + List getSysOperationLogList( + @Param("sysOperationLogFilter") SysOperationLog sysOperationLogFilter, + @Param("orderBy") String orderBy); +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml new file mode 100644 index 00000000..f29559f1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dao/mapper/SysOperationLogMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_sys_operation_log.operation_type = #{sysOperationLogFilter.operationType} + + + + AND zz_sys_operation_log.request_url LIKE #{safeRequestUrl} + + + AND zz_sys_operation_log.trace_id = #{sysOperationLogFilter.traceId} + + + AND zz_sys_operation_log.success = #{sysOperationLogFilter.success} + + + + AND zz_sys_operation_log.operator_name LIKE #{safeOperatorName} + + + AND zz_sys_operation_log.elapse >= #{sysOperationLogFilter.elapseMin} + + + AND zz_sys_operation_log.elapse <= #{sysOperationLogFilter.elapseMax} + + + AND zz_sys_operation_log.operation_time >= #{sysOperationLogFilter.operationTimeStart} + + + AND zz_sys_operation_log.operation_time <= #{sysOperationLogFilter.operationTimeEnd} + + + + + + INSERT INTO zz_sys_operation_log VALUES + + (#{item.logId}, + #{item.description}, + #{item.operationType}, + #{item.serviceName}, + #{item.apiClass}, + #{item.apiMethod}, + #{item.sessionId}, + #{item.traceId}, + #{item.elapse}, + #{item.requestMethod}, + #{item.requestUrl}, + #{item.requestArguments}, + #{item.responseResult}, + #{item.requestIp}, + #{item.success}, + #{item.errorMsg}, + #{item.tenantId}, + #{item.operatorId}, + #{item.operatorName}, + #{item.operationTime}) + + + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java new file mode 100644 index 00000000..994f51f0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/dto/SysOperationLogDto.java @@ -0,0 +1,77 @@ +package com.orangeforms.common.log.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 操作日志记录表 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "操作日志Dto") +@Data +public class SysOperationLogDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long logId; + + /** + * 操作类型。 + * 常量值定义可参考SysOperationLogType对象。 + */ + @Schema(description = "操作类型") + private Integer operationType; + + /** + * 每次请求的Id。 + * 对于微服务之间的调用,在同一个请求的调用链中,该值是相同的。 + */ + @Schema(description = "每次请求的Id") + private String traceId; + + /** + * HTTP 请求地址。 + */ + @Schema(description = "HTTP 请求地址") + private String requestUrl; + + /** + * 应答状态。 + */ + @Schema(description = "应答状态") + private Boolean success; + + /** + * 操作员名称。 + */ + @Schema(description = "操作员名称") + private String operatorName; + + /** + * 调用时长最小值。 + */ + @Schema(description = "调用时长最小值") + private Long elapseMin; + + /** + * 调用时长最大值。 + */ + @Schema(description = "调用时长最大值") + private Long elapseMax; + + /** + * 操作开始时间。 + */ + @Schema(description = "操作开始时间") + private String operationTimeStart; + + /** + * 操作开始时间。 + */ + @Schema(description = "操作开始时间") + private String operationTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java new file mode 100644 index 00000000..b1b4217e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/SysOperationLog.java @@ -0,0 +1,170 @@ +package com.orangeforms.common.log.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.TenantFilterColumn; +import lombok.Data; + +import java.util.Date; + +/** + * 操作日志记录表 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName("zz_sys_operation_log") +public class SysOperationLog { + + /** + * 主键Id。 + */ + @TableId(value = "log_id") + private Long logId; + + /** + * 日志描述。 + */ + @TableField(value = "description") + private String description; + + /** + * 操作类型。 + * 常量值定义可参考SysOperationLogType对象。 + */ + @TableField(value = "operation_type") + private Integer operationType; + + /** + * 接口所在服务名称。 + * 通常为spring.application.name配置项的值。 + */ + @TableField(value = "service_name") + private String serviceName; + + /** + * 调用的controller全类名。 + * 之所以为独立字段,是为了便于查询和统计接口的调用频度。 + */ + @TableField(value = "api_class") + private String apiClass; + + /** + * 调用的controller中的方法。 + * 格式为:接口类名 + "." + 方法名。 + */ + @TableField(value = "api_method") + private String apiMethod; + + /** + * 用户会话sessionId。 + * 主要是为了便于统计,以及跟踪查询定位问题。 + */ + @TableField(value = "session_id") + private String sessionId; + + /** + * 每次请求的Id。 + * 对于微服务之间的调用,在同一个请求的调用链中,该值是相同的。 + */ + @TableField(value = "trace_id") + private String traceId; + + /** + * 调用时长。 + */ + @TableField(value = "elapse") + private Long elapse; + + /** + * HTTP 请求方法,如GET。 + */ + @TableField(value = "request_method") + private String requestMethod; + + /** + * HTTP 请求地址。 + */ + @TableField(value = "request_url") + private String requestUrl; + + /** + * controller接口参数。 + */ + @TableField(value = "request_arguments") + private String requestArguments; + + /** + * controller应答结果。 + */ + @TableField(value = "response_result") + private String responseResult; + + /** + * 请求IP。 + */ + @TableField(value = "request_ip") + private String requestIp; + + /** + * 应答状态。 + */ + @TableField(value = "success") + private Boolean success; + + /** + * 错误信息。 + */ + @TableField(value = "error_msg") + private String errorMsg; + + /** + * 租户Id。 + * 仅用于多租户系统,是便于进行对租户的操作查询和统计分析。 + */ + @TenantFilterColumn + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 操作员Id。 + */ + @TableField(value = "operator_id") + private Long operatorId; + + /** + * 操作员名称。 + */ + @TableField(value = "operator_name") + private String operatorName; + + /** + * 操作时间。 + */ + @TableField(value = "operation_time") + private Date operationTime; + + /** + * 调用时长最小值。 + */ + @TableField(exist = false) + private Long elapseMin; + + /** + * 调用时长最大值。 + */ + @TableField(exist = false) + private Long elapseMax; + + /** + * 操作开始时间。 + */ + @TableField(exist = false) + private String operationTimeStart; + + /** + * 操作结束时间。 + */ + @TableField(exist = false) + private String operationTimeEnd; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java new file mode 100644 index 00000000..ec3edaf5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/model/constant/SysOperationLogType.java @@ -0,0 +1,145 @@ +package com.orangeforms.common.log.model.constant; + +/** + * 操作日志类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class SysOperationLogType { + + /** + * 其他。 + */ + public static final int OTHER = -1; + /** + * 登录。 + */ + public static final int LOGIN = 0; + /** + * 登录移动端。 + */ + public static final int LOGIN_MOBILE = 1; + /** + * 登出。 + */ + public static final int LOGOUT = 5; + /** + * 登出移动端。 + */ + public static final int LOGOUT_MOBILE = 6; + /** + * 新增。 + */ + public static final int ADD = 10; + /** + * 修改。 + */ + public static final int UPDATE = 15; + /** + * 删除。 + */ + public static final int DELETE = 20; + /** + * 批量删除。 + */ + public static final int DELETE_BATCH = 21; + /** + * 新增多对多关联。 + */ + public static final int ADD_M2M = 25; + /** + * 移除多对多关联。 + */ + public static final int DELETE_M2M = 30; + /** + * 批量移除多对多关联。 + */ + public static final int DELETE_M2M_BATCH = 31; + /** + * 查询。 + */ + public static final int LIST = 35; + /** + * 分组查询。 + */ + public static final int LIST_WITH_GROUP = 40; + /** + * 导出。 + */ + public static final int EXPORT = 45; + /** + * 导入。 + */ + public static final int IMPORT = 46; + /** + * 上传。 + */ + public static final int UPLOAD = 50; + /** + * 下载。 + */ + public static final int DOWNLOAD = 55; + /** + * 重置缓存。 + */ + public static final int RELOAD_CACHE = 60; + /** + * 发布。 + */ + public static final int PUBLISH = 65; + /** + * 取消发布。 + */ + public static final int UNPUBLISH = 70; + /** + * 暂停。 + */ + public static final int SUSPEND = 75; + /** + * 恢复。 + */ + public static final int RESUME = 80; + /** + * 启动流程。 + */ + public static final int START_FLOW = 100; + /** + * 停止流程。 + */ + public static final int STOP_FLOW = 105; + /** + * 删除流程。 + */ + public static final int DELETE_FLOW = 110; + /** + * 取消流程。 + */ + public static final int CANCEL_FLOW = 115; + /** + * 提交任务。 + */ + public static final int SUBMIT_TASK = 120; + /** + * 催办任务。 + */ + public static final int REMIND_TASK = 125; + /** + * 干预任务。 + */ + public static final int INTERVENE_FLOW = 126; + /** + * 修复流程的业务数据。 + */ + public static final int FIX_FLOW_BUSINESS_DATA = 127; + /** + * 流程复活。 + */ + public static final int REVIVE_FLOW = 128; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private SysOperationLogType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java new file mode 100644 index 00000000..18c1b087 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/SysOperationLogService.java @@ -0,0 +1,45 @@ +package com.orangeforms.common.log.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.log.model.SysOperationLog; + +import java.util.List; + +/** + * 操作日志服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface SysOperationLogService extends IBaseService { + + /** + * 异步的插入一条新操作日志。 + * + * @param operationLog 操作日志对象。 + */ + void saveNewAsync(SysOperationLog operationLog); + + /** + * 插入一条新操作日志。 + * + * @param operationLog 操作日志对象。 + */ + void saveNew(SysOperationLog operationLog); + + /** + * 批量插入。 + * + * @param sysOperationLogList 操作日志列表。 + */ + void batchSave(List sysOperationLogList); + + /** + * 根据过滤条件和排序规则,查询操作日志。 + * + * @param filter 操作日志的过滤对象。 + * @param orderBy 排序规则。 + * @return 查询列表。 + */ + List getSysOperationLogList(SysOperationLog filter, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java new file mode 100644 index 00000000..3935df68 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/service/impl/SysOperationLogServiceImpl.java @@ -0,0 +1,84 @@ +package com.orangeforms.common.log.service.impl; + +import com.orangeforms.common.core.annotation.MyDataSource; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.log.dao.SysOperationLogMapper; +import com.orangeforms.common.log.model.SysOperationLog; +import com.orangeforms.common.log.service.SysOperationLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 操作日志服务实现类。 + * 这里需要重点解释下MyDataSource注解。在单数据源服务中,由于没有DataSourceAspect的切面类,所以该注解不会 + * 有任何作用和影响。然而在多数据源情况下,由于每个服务都有自己的DataSourceType常量对象,表示不同的数据源。 + * 而common-log在公用模块中,不能去依赖业务服务,因此这里给出了一个固定值。我们在业务的DataSourceType中,也要 + * 使用该值ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE,去关联操作日志所需的数据源配置。 + * + * @author Jerry + * @date 2024-07-02 + */ +@MyDataSource(ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE) +@Service +public class SysOperationLogServiceImpl extends BaseService implements SysOperationLogService { + + @Autowired + private SysOperationLogMapper sysOperationLogMapper; + + @Override + protected BaseDaoMapper mapper() { + return sysOperationLogMapper; + } + + /** + * 异步插入一条新操作日志。通常用于在橙单中创建的单体工程服务。 + * + * @param operationLog 操作日志对象。 + */ + @Async + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewAsync(SysOperationLog operationLog) { + sysOperationLogMapper.insert(operationLog); + } + + /** + * 插入一条新操作日志。 + * + * @param operationLog 操作日志对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNew(SysOperationLog operationLog) { + sysOperationLogMapper.insert(operationLog); + } + + /** + * 批量插入。通常用于在橙单中创建的微服务工程服务。 + * + * @param sysOperationLogList 操作日志列表。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void batchSave(List sysOperationLogList) { + sysOperationLogMapper.insertList(sysOperationLogList); + } + + /** + * 根据过滤条件和排序规则,查询操作日志。 + * + * @param filter 操作日志的过滤对象。 + * @param orderBy 排序规则。 + * @return 查询列表。 + */ + @Override + public List getSysOperationLogList(SysOperationLog filter, String orderBy) { + return sysOperationLogMapper.getSysOperationLogList(filter, orderBy); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java new file mode 100644 index 00000000..983ea9ed --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/java/com/orangeforms/common/log/vo/SysOperationLogVo.java @@ -0,0 +1,144 @@ +package com.orangeforms.common.log.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 操作日志记录表 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "操作日志VO") +@Data +public class SysOperationLogVo { + + /** + * 操作日志主键Id。 + */ + @Schema(description = "操作日志主键Id") + private Long logId; + + /** + * 日志描述。 + */ + @Schema(description = "日志描述") + private String description; + + /** + * 操作类型。 + * 常量值定义可参考SysOperationLogType对象。 + */ + @Schema(description = "操作类型") + private Integer operationType; + + /** + * 接口所在服务名称。 + * 通常为spring.application.name配置项的值。 + */ + @Schema(description = "接口所在服务名称") + private String serviceName; + + /** + * 调用的controller全类名。 + * 之所以为独立字段,是为了便于查询和统计接口的调用频度。 + */ + @Schema(description = "调用的controller全类名") + private String apiClass; + + /** + * 调用的controller中的方法。 + * 格式为:接口类名 + "." + 方法名。 + */ + @Schema(description = "调用的controller中的方法") + private String apiMethod; + + /** + * 用户会话sessionId。 + * 主要是为了便于统计,以及跟踪查询定位问题。 + */ + @Schema(description = "用户会话sessionId") + private String sessionId; + + /** + * 每次请求的Id。 + * 对于微服务之间的调用,在同一个请求的调用链中,该值是相同的。 + */ + @Schema(description = "每次请求的Id") + private String traceId; + + /** + * 调用时长。 + */ + @Schema(description = "调用时长") + private Long elapse; + + /** + * HTTP 请求方法,如GET。 + */ + @Schema(description = "HTTP 请求方法") + private String requestMethod; + + /** + * HTTP 请求地址。 + */ + @Schema(description = "HTTP 请求地址") + private String requestUrl; + + /** + * controller接口参数。 + */ + @Schema(description = "controller接口参数") + private String requestArguments; + + /** + * controller应答结果。 + */ + @Schema(description = "controller应答结果") + private String responseResult; + + /** + * 请求IP。 + */ + @Schema(description = "请求IP") + private String requestIp; + + /** + * 应答状态。 + */ + @Schema(description = "应答状态") + private Boolean success; + + /** + * 错误信息。 + */ + @Schema(description = "错误信息") + private String errorMsg; + + /** + * 租户Id。 + * 仅用于多租户系统,是便于进行对租户的操作查询和统计分析。 + */ + @Schema(description = "租户Id") + private Long tenantId; + + /** + * 操作员Id。 + */ + @Schema(description = "操作员Id") + private Long operatorId; + + /** + * 操作员名称。 + */ + @Schema(description = "操作员名称") + private String operatorName; + + /** + * 操作时间。 + */ + @Schema(description = "操作时间") + private Date operationTime; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..dff1b36f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.log.config.CommonLogAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-minio/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-minio/pom.xml new file mode 100644 index 00000000..178b8c8e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-minio/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-minio + 1.0.0 + common-minio + jar + + + + io.minio + minio + ${minio.version} + + + com.orangeforms + common-core + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java new file mode 100644 index 00000000..d89019ff --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioAutoConfiguration.java @@ -0,0 +1,56 @@ +package com.orangeforms.common.minio.config; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.minio.wrapper.MinioTemplate; +import io.minio.BucketExistsArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * common-minio模块的自动配置引导类。仅当配置项minio.enabled为true的时候加载。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties(MinioProperties.class) +@ConditionalOnProperty(prefix = "minio", name = "enabled") +public class MinioAutoConfiguration { + + /** + * 将minio原生的客户端类封装成bean对象,便于集成,同时也可以灵活使用客户端的所有功能。 + * + * @param p 属性配置对象。 + * @return minio的原生客户端对象。 + */ + @Bean + @ConditionalOnMissingBean + public MinioClient minioClient(MinioProperties p) { + try { + MinioClient client = MinioClient.builder() + .endpoint(p.getEndpoint()).credentials(p.getAccessKey(), p.getSecretKey()).build(); + if (!client.bucketExists(BucketExistsArgs.builder().bucket(p.getBucketName()).build())) { + client.makeBucket(MakeBucketArgs.builder().bucket(p.getBucketName()).build()); + } + return client; + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 封装的minio模板类。 + * + * @param p 属性配置对象。 + * @param c minio的原生客户端bean对象。 + * @return minio模板的bean对象。 + */ + @Bean + @ConditionalOnMissingBean + public MinioTemplate minioTemplate(MinioProperties p, MinioClient c) { + return new MinioTemplate(p, c); + } +} \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java new file mode 100644 index 00000000..ecdf253d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/config/MinioProperties.java @@ -0,0 +1,32 @@ +package com.orangeforms.common.minio.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common-minio模块的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "minio") +public class MinioProperties { + + /** + * 访问入口地址。 + */ + private String endpoint; + /** + * 访问安全的key。 + */ + private String accessKey; + /** + * 访问安全的密钥。 + */ + private String secretKey; + /** + * 缺省桶名称。 + */ + private String bucketName; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java new file mode 100644 index 00000000..9c2c71a7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/util/MinioUpDownloader.java @@ -0,0 +1,115 @@ +package com.orangeforms.common.minio.util; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.minio.wrapper.MinioTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; + +/** + * 基于Minio上传和下载文件操作的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +@ConditionalOnProperty(prefix = "minio", name = "enabled") +public class MinioUpDownloader extends BaseUpDownloader { + + @Autowired + private MinioTemplate minioTemplate; + @Autowired + private UpDownloaderFactory factory; + + @PostConstruct + public void doRegister() { + factory.registerUpDownloader(UploadStoreTypeEnum.MINIO_SYSTEM, this); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + String uploadPath = super.makeFullPath(null, modelName, fieldName, asImage); + return this.doUploadInternally(serviceContextPath, uploadPath, asImage, uploadFile); + } + + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String uriPath, + MultipartFile uploadFile) throws IOException { + String uploadPath = super.makeFullPath(null, uriPath); + return this.doUploadInternally(serviceContextPath, uploadPath, false, uploadFile); + } + + @Override + public void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) throws IOException { + String uploadPath = this.makeFullPath(null, modelName, fieldName, asImage); + String fullFileanme = uploadPath + "/" + fileName; + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException { + StringBuilder pathBuilder = new StringBuilder(128); + if (StrUtil.isNotBlank(uriPath)) { + pathBuilder.append(uriPath); + } + pathBuilder.append("/"); + String fullFileanme = pathBuilder.append(fileName).toString(); + this.downloadInternal(fullFileanme, fileName, response); + } + + private UploadResponseInfo doUploadInternally( + String serviceContextPath, + String uploadPath, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = super.verifyUploadArgument(asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + return responseInfo; + } + responseInfo.setUploadPath(uploadPath); + super.fillUploadResponseInfo(responseInfo, serviceContextPath, uploadFile.getOriginalFilename()); + minioTemplate.putObject(uploadPath + "/" + responseInfo.getFilename(), uploadFile.getInputStream()); + return responseInfo; + } + + private void downloadInternal(String fullFileanme, String fileName, HttpServletResponse response) throws IOException { + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + InputStream in = minioTemplate.getStream(fullFileanme); + IoUtil.copy(in, response.getOutputStream()); + in.close(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java new file mode 100644 index 00000000..dc29310f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/java/com/orangeforms/common/minio/wrapper/MinioTemplate.java @@ -0,0 +1,199 @@ +package com.orangeforms.common.minio.wrapper; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.minio.config.MinioProperties; +import io.minio.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 封装的minio客户端模板类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class MinioTemplate { + + private static final String TMP_DIR = System.getProperty("java.io.tmpdir") + File.separator; + private final MinioProperties properties; + private final MinioClient client; + + public MinioTemplate(MinioProperties properties, MinioClient client) { + super(); + this.properties = properties; + this.client = client; + } + + /** + * 判断bucket是否存在。 + * + * @param bucketName 桶名称。 + * @return 存在返回true,否则false。 + */ + public boolean bucketExists(String bucketName) { + try { + return client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 创建桶。 + * + * @param bucketName 桶名称。 + */ + public void makeBucket(String bucketName) { + try { + if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { + client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 存放对象。 + * + * @param bucketName 桶名称。 + * @param objectName 对象名称。 + * @param filename 本地上传的文件名称。 + */ + public void putObject(String bucketName, String objectName, String filename) { + try { + this.putObject(bucketName, objectName, new FileInputStream(filename)); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 存放对象。桶名称为配置中的桶名称。 + * + * @param objectName 对象名称。 + * @param filename 本地上传的文件名称。 + */ + public void putObject(String objectName, String filename) { + try { + this.putObject(properties.getBucketName(), objectName, filename); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 读取输入流并存放。 + * + * @param bucketName 桶名称。 + * @param objectName 对象名称。 + * @param stream 读取后上传的文件流。 + */ + public void putObject(String bucketName, String objectName, InputStream stream) { + try { + client.putObject(PutObjectArgs.builder() + .bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } finally { + try { + stream.close(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + } + + /** + * 读取输入流并存放。 + * + * @param objectName 对象名称。 + * @param stream 读取后上传的文件流。 + */ + public void putObject(String objectName, InputStream stream) { + this.putObject(properties.getBucketName(), objectName, stream); + } + + /** + * 移除对象。 + * + * @param bucketName 桶名称。 + * @param objectName 对象名称。 + */ + public void removeObject(String bucketName, String objectName) { + try { + client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 移除对象。桶名称为配置中的桶名称。 + * + * @param objectName 对象名称。 + */ + public void removeObject(String objectName) { + this.removeObject(properties.getBucketName(), objectName); + } + + /** + * 获取文件输入流。 + * + * @param bucket 桶名称。 + * @param objectName 对象名称。 + * @return 文件的输入流。 + */ + public InputStream getStream(String bucket, String objectName) { + try { + return client.getObject(GetObjectArgs.builder().bucket(bucket).object(objectName).build()); + } catch (Exception e) { + throw new MyRuntimeException(e); + } + } + + /** + * 获取文件输入流。 + * + * @param objectName 对象名称。 + * @return 文件的输入流。 + */ + public InputStream getStream(String objectName) { + return this.getStream(properties.getBucketName(), objectName); + } + + /** + * 获取存储的文件对象。 + * + * @param bucket 桶名称。 + * @param objectName 对象名称。 + * @return 读取后存储到文件的文件对象。 + */ + public File getFile(String bucket, String objectName) throws IOException { + InputStream in = getStream(bucket, objectName); + File dir = new File(TMP_DIR); + if (!dir.exists() || dir.isFile()) { + dir.mkdirs(); + } + File file = new File(TMP_DIR + objectName); + FileUtils.copyInputStreamToFile(in, file); + in.close(); + return file; + } + + /** + * 获取存储的文件对象。桶名称为配置中的桶名称。 + * + * @param objectName 对象名称。 + * @return 读取后存储到文件的文件对象。 + */ + public File getFile(String objectName) throws IOException { + return this.getFile(properties.getBucketName(), objectName); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..a7ba3af4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-minio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.minio.config.MinioAutoConfiguration \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/pom.xml new file mode 100644 index 00000000..c653f38f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/pom.xml @@ -0,0 +1,64 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-online + 1.0.0 + common-online + jar + + + + com.orangeforms + common-satoken + 1.0.0 + + + com.orangeforms + common-dbutil + 1.0.0 + + + com.orangeforms + common-dict + 1.0.0 + + + com.orangeforms + common-datafilter + 1.0.0 + + + com.orangeforms + common-redis + 1.0.0 + + + com.orangeforms + common-sequence + 1.0.0 + + + com.orangeforms + common-log + 1.0.0 + + + com.orangeforms + common-minio + 1.0.0 + + + com.orangeforms + common-swagger + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java new file mode 100644 index 00000000..2f18a739 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineAutoConfig.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-online模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({OnlineProperties.class}) +public class OnlineAutoConfig { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java new file mode 100644 index 00000000..17308333 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/config/OnlineProperties.java @@ -0,0 +1,59 @@ +package com.orangeforms.common.online.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + * 在线表单的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-online") +public class OnlineProperties { + + /** + * 脱敏字段的掩码。只能为单个字符。 + */ + private String maskChar = "*"; + /** + * 在调用render接口的时候,是否打开一级缓存加速页面渲染数据的获取。 + */ + private Boolean enableRenderCache = true; + /** + * 业务表和在线表单内置表是否跨库。 + */ + private Boolean enabledMultiDatabaseWrite = true; + /** + * 仅以该前缀开头的数据表才会成为动态表单的候选数据表,如: zz_。如果为空,则所有表均可被选。 + */ + private String tablePrefix; + /** + * 在线表单业务操作的URL前缀。 + */ + private String urlPrefix; + /** + * 在线表单打印接口的路径 + */ + private String printUrlPath; + /** + * 上传文件的根路径。 + */ + private String uploadFileBaseDir; + /** + * 1: minio 2: aliyun-oss 3: qcloud-cos。 + * 0是本地系统,不推荐使用。 + */ + private Integer distributeStoreType; + /** + * 在线表单查看权限的URL列表。 + */ + private List viewUrlList; + /** + * 在线表单编辑权限的URL列表。 + */ + private List editUrlList; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java new file mode 100644 index 00000000..52c169db --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineColumnController.java @@ -0,0 +1,517 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.MaskFieldTypeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineColumnDto; +import com.orangeforms.common.online.dto.OnlineColumnRuleDto; +import com.orangeforms.common.online.dto.OnlineRuleDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineColumnRuleVo; +import com.orangeforms.common.online.vo.OnlineColumnVo; +import com.orangeforms.common.online.vo.OnlineRuleVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 在线表单字段数据接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单字段数据接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineColumn") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineColumnController { + + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineRuleService onlineRuleService; + @Autowired + private OnlineDictService onlineDictService; + + /** + * 根据数据库表字段信息,在指定在线表中添加在线表字段对象。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 数据库表名称。 + * @param columnName 数据库表字段名。 + * @param tableId 目的表Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody Long dblinkId, + @MyRequestBody String tableName, + @MyRequestBody String columnName, + @MyRequestBody Long tableId) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMessage; + SqlTableColumn sqlTableColumn = onlineDblinkService.getDblinkTableColumn(dblink, tableName, columnName); + if (sqlTableColumn == null) { + errorMessage = "数据验证失败,指定的数据表字段不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult verifyResult = this.doVerifyTable(tableId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + onlineColumnService.saveNewList(CollUtil.newLinkedList(sqlTableColumn), tableId); + return ResponseResult.success(); + } + + /** + * 更新字段数据数据。 + * + * @param onlineColumnDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineColumnDto onlineColumnDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineColumnDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineColumn onlineColumn = MyModelUtil.copyTo(onlineColumnDto, OnlineColumn.class); + OnlineColumn originalOnlineColumn = onlineColumnService.getById(onlineColumn.getColumnId()); + if (originalOnlineColumn == null) { + errorMessage = "数据验证失败,当前在线表字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyColumnResult = this.doVerifyColumn(onlineColumn, originalOnlineColumn); + if (!verifyColumnResult.isSuccess()) { + return ResponseResult.errorFrom(verifyColumnResult); + } + ResponseResult verifyResult = this.doVerifyTable(originalOnlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineColumnService.update(onlineColumn, originalOnlineColumn)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除字段数据数据。 + * + * @param columnId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long columnId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(columnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineColumn originalOnlineColumn = onlineColumnService.getById(columnId); + if (originalOnlineColumn == null) { + errorMessage = "数据验证失败,当前在线表字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerifyTable(originalOnlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineVirtualColumn virtualColumnFilter = new OnlineVirtualColumn(); + virtualColumnFilter.setAggregationColumnId(columnId); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnList(virtualColumnFilter, null); + if (CollUtil.isNotEmpty(virtualColumnList)) { + OnlineVirtualColumn virtualColumn = virtualColumnList.get(0); + errorMessage = "数据验证失败,数据源关联正在被虚拟字段 [" + virtualColumn.getColumnPrompt() + "] 使用,不能被删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineColumnService.remove(originalOnlineColumn.getTableId(), columnId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的字段数据列表。 + * + * @param onlineColumnDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineColumnDto onlineColumnDtoFilter, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineColumn onlineColumnFilter = MyModelUtil.copyTo(onlineColumnDtoFilter, OnlineColumn.class); + List onlineColumnList = + onlineColumnService.getOnlineColumnListWithRelation(onlineColumnFilter); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineColumnList, OnlineColumnVo.class)); + } + + /** + * 查看指定字段数据对象详情。 + * + * @param columnId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long columnId) { + if (MyCommonUtil.existBlankArgument(columnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineColumn onlineColumn = onlineColumnService.getByIdWithRelation(columnId, MyRelationParam.full()); + if (onlineColumn == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineColumn, OnlineColumnVo.class); + } + + /** + * 将数据库中的表字段信息刷新到已经导入的在线表字段信息。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 数据库表名称。 + * @param columnName 数据库表字段名。 + * @param columnId 被刷新的在线字段Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/refresh") + public ResponseResult refresh( + @MyRequestBody Long dblinkId, + @MyRequestBody String tableName, + @MyRequestBody String columnName, + @MyRequestBody Long columnId) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + String errorMsg; + SqlTableColumn sqlTableColumn = onlineDblinkService.getDblinkTableColumn(dblink, tableName, columnName); + if (sqlTableColumn == null) { + errorMsg = "数据验证失败,指定的数据表字段不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMsg); + } + OnlineColumn onlineColumn = onlineColumnService.getById(columnId); + if (onlineColumn == null) { + errorMsg = "数据验证失败,指定的在线表字段Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMsg); + } + ResponseResult verifyResult = this.doVerifyTable(onlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + onlineColumnService.refresh(sqlTableColumn, onlineColumn); + return ResponseResult.success(); + } + + /** + * 列出不与指定字段数据存在多对多关系的 [验证规则] 列表数据。通常用于查看添加新 [验证规则] 对象的候选列表。 + * + * @param columnId 主表关联字段。 + * @param onlineRuleDtoFilter [验证规则] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/listNotInOnlineColumnRule") + public ResponseResult> listNotInOnlineColumnRule( + @MyRequestBody Long columnId, + @MyRequestBody OnlineRuleDto onlineRuleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineRule filter = MyModelUtil.copyTo(onlineRuleDtoFilter, OnlineRule.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineRule.class); + List onlineRuleList = + onlineRuleService.getNotInOnlineRuleListByColumnId(columnId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineRuleList, OnlineRuleVo.class)); + } + + /** + * 列出与指定字段数据存在多对多关系的 [验证规则] 列表数据。 + * + * @param columnId 主表关联字段。 + * @param onlineRuleDtoFilter [验证规则] 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/listOnlineColumnRule") + public ResponseResult> listOnlineColumnRule( + @MyRequestBody Long columnId, + @MyRequestBody OnlineRuleDto onlineRuleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineRule filter = MyModelUtil.copyTo(onlineRuleDtoFilter, OnlineRule.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineRule.class); + List onlineRuleList = + onlineRuleService.getOnlineRuleListByColumnId(columnId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineRuleList, OnlineRuleVo.class)); + } + + /** + * 批量添加字段数据和 [验证规则] 对象的多对多关联关系数据。 + * + * @param columnId 主表主键Id。 + * @param onlineColumnRuleDtoList 关联对象列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addOnlineColumnRule") + public ResponseResult addOnlineColumnRule( + @MyRequestBody Long columnId, @MyRequestBody List onlineColumnRuleDtoList) { + if (MyCommonUtil.existBlankArgument(columnId, onlineColumnRuleDtoList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + String errorMessage; + for (OnlineColumnRuleDto onlineColumnRule : onlineColumnRuleDtoList) { + errorMessage = MyCommonUtil.getModelValidationError(onlineColumnRule); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + Set ruleIdSet = onlineColumnRuleDtoList.stream() + .map(OnlineColumnRuleDto::getRuleId).collect(Collectors.toSet()); + List ruleList = onlineRuleService.getInList(ruleIdSet); + if (ruleIdSet.size() != ruleList.size()) { + errorMessage = "数据验证失败,参数中存在非法字段规则Id!"; + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID, errorMessage); + } + for (OnlineRule rule : ruleList) { + if (BooleanUtil.isFalse(rule.getBuiltin()) + && !StrUtil.equals(rule.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,参数中存在不属于该应用的字段规则Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + List onlineColumnRuleList = + MyModelUtil.copyCollectionTo(onlineColumnRuleDtoList, OnlineColumnRule.class); + onlineColumnService.addOnlineColumnRuleList(onlineColumnRuleList, columnId); + return ResponseResult.success(); + } + + /** + * 更新指定字段数据和指定 [验证规则] 的多对多关联数据。 + * + * @param onlineColumnRuleDto 对多对中间表对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateOnlineColumnRule") + public ResponseResult updateOnlineColumnRule(@MyRequestBody OnlineColumnRuleDto onlineColumnRuleDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineColumnRuleDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult verifyResult = this.doVerifyColumn(onlineColumnRuleDto.getColumnId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineColumnRule onlineColumnRule = MyModelUtil.copyTo(onlineColumnRuleDto, OnlineColumnRule.class); + if (!onlineColumnService.updateOnlineColumnRule(onlineColumnRule)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 显示字段数据和指定 [验证规则] 的多对多关联详情数据。 + * + * @param columnId 主表主键Id。 + * @param ruleId 从表主键Id。 + * @return 应答结果对象,包括中间表详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/viewOnlineColumnRule") + public ResponseResult viewOnlineColumnRule( + @RequestParam Long columnId, @RequestParam Long ruleId) { + if (MyCommonUtil.existBlankArgument(columnId, ruleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineColumnRule onlineColumnRule = onlineColumnService.getOnlineColumnRule(columnId, ruleId); + if (onlineColumnRule == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + OnlineColumnRuleVo onlineColumnRuleVo = MyModelUtil.copyTo(onlineColumnRule, OnlineColumnRuleVo.class); + return ResponseResult.success(onlineColumnRuleVo); + } + + /** + * 移除指定字段数据和指定 [验证规则] 的多对多关联关系。 + * + * @param columnId 主表主键Id。 + * @param ruleId 从表主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteOnlineColumnRule") + public ResponseResult deleteOnlineColumnRule(@MyRequestBody Long columnId, @MyRequestBody Long ruleId) { + if (MyCommonUtil.existBlankArgument(columnId, ruleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyColumn(columnId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineColumnService.removeOnlineColumnRule(columnId, ruleId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 以字典形式返回全部字段数据数据集合。字典的键值为[columnId, columnName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject OnlineColumnDto filter) { + List resultList = + onlineColumnService.getListByFilter(MyModelUtil.copyTo(filter, OnlineColumn.class)); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, OnlineColumn::getColumnId, OnlineColumn::getColumnName)); + } + + private ResponseResult doVerifyColumn(Long columnId) { + if (MyCommonUtil.existBlankArgument(columnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineColumn onlineColumn = onlineColumnService.getById(columnId); + if (onlineColumn == null) { + return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID); + } + ResponseResult verifyResult = this.doVerifyTable(onlineColumn.getTableId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyColumn(OnlineColumn onlineColumn, OnlineColumn originalOnlineColumn) { + String errorMessage; + if (onlineColumn.getDictId() != null + && ObjectUtil.notEqual(onlineColumn.getDictId(), originalOnlineColumn.getDictId())) { + OnlineDict dict = onlineDictService.getById(onlineColumn.getDictId()); + if (dict == null) { + errorMessage = "数据验证失败,关联的字典Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!StrUtil.equals(dict.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,关联的字典Id并不属于当前应用!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + } + if (MyCommonUtil.equalsAny(onlineColumn.getFieldKind(), FieldKind.UPLOAD, FieldKind.UPLOAD_IMAGE) + && onlineColumn.getUploadFileSystemType() == null) { + errorMessage = "数据验证失败,上传字段必须设置上传文件系统类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.equal(onlineColumn.getFieldKind(), FieldKind.MASK_FIELD)) { + if (onlineColumn.getMaskFieldType() == null) { + errorMessage = "数据验证失败,脱敏字段没有设置脱敏类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!EnumUtil.contains(MaskFieldTypeEnum.class, onlineColumn.getMaskFieldType())) { + errorMessage = "数据验证失败,脱敏字段设置的脱敏类型并不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + if (!onlineColumn.getTableId().equals(originalOnlineColumn.getTableId())) { + errorMessage = "数据验证失败,字段的所属表Id不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyTable(Long tableId) { + String errorMessage; + OnlineTable table = onlineTableService.getById(tableId); + if (table == null) { + errorMessage = "数据验证失败,指定的数据表Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(table.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用并不包含该字段所在的表!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java new file mode 100644 index 00000000..18831b3b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceController.java @@ -0,0 +1,287 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDatasourceDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.PageType; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineDatasourceVo; +import com.orangeforms.common.online.vo.OnlineTableVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单数据源接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据源接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDatasource") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDatasourceController { + + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineDblinkService onlineDblinkService; + + /** + * 新增数据模型数据。 + * + * @param onlineDatasourceDto 新增对象。 + * @param pageId 关联的页面Id。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineDatasourceDto.datasourceId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add( + @MyRequestBody OnlineDatasourceDto onlineDatasourceDto, + @MyRequestBody(required = true) Long pageId) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDatasourceDto, Default.class, AddGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlinePage onlinePage = onlinePageService.getById(pageId); + if (onlinePage == null) { + errorMessage = "数据验证失败,页面Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + if (!StrUtil.equals(onlinePage.getAppCode(), appCode)) { + errorMessage = "数据验证失败,当前应用并不存在该页面!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasource onlineDatasource = MyModelUtil.copyTo(onlineDatasourceDto, OnlineDatasource.class); + if (onlineDatasourceService.existByVariableName(onlineDatasource.getVariableName())) { + errorMessage = "数据验证失败,当前数据源变量已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + OnlineDblink onlineDblink = onlineDblinkService.getById(onlineDatasourceDto.getDblinkId()); + if (onlineDblink == null) { + errorMessage = "数据验证失败,关联的数据库链接Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(onlineDblink.getAppCode(), appCode)) { + errorMessage = "数据验证失败,当前应用并不存在该数据库链接!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SqlTable sqlTable = onlineDblinkService.getDblinkTable(onlineDblink, onlineDatasourceDto.getMasterTableName()); + if (sqlTable == null) { + errorMessage = "数据验证失败,指定的数据表名不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + ResponseResult verifyResult = this.doVerifyPrimaryKey(sqlTable, onlinePage); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + try { + onlineDatasource = onlineDatasourceService.saveNew(onlineDatasource, sqlTable, pageId); + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的数据源变量名已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(onlineDatasource.getDatasourceId()); + } + + /** + * 更新数据模型数据。 + * + * @param onlineDatasourceDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDatasourceDto onlineDatasourceDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDatasourceDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasource onlineDatasource = MyModelUtil.copyTo(onlineDatasourceDto, OnlineDatasource.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineDatasource.getDatasourceId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasource originalOnlineDatasource = verifyResult.getData(); + if (!onlineDatasource.getDblinkId().equals(originalOnlineDatasource.getDblinkId())) { + errorMessage = "数据验证失败,不能修改数据库链接Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasource.getMasterTableId().equals(originalOnlineDatasource.getMasterTableId())) { + errorMessage = "数据验证失败,不能修改主表Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!StrUtil.equals(onlineDatasource.getVariableName(), originalOnlineDatasource.getVariableName()) + && onlineDatasourceService.existByVariableName(onlineDatasource.getVariableName())) { + errorMessage = "数据验证失败,当前数据源变量已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + try { + if (!onlineDatasourceService.update(onlineDatasource, originalOnlineDatasource)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的数据源变量名已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 删除数据模型数据。 + * + * @param datasourceId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long datasourceId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + ResponseResult verifyResult = this.doVerifyAndGet(datasourceId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + List formList = onlineFormService.getOnlineFormListByDatasourceId(datasourceId); + if (CollUtil.isNotEmpty(formList)) { + errorMessage = "数据验证失败,当前数据源正在被 [" + formList.get(0).getFormName() + "] 表单占用,请先删除关联数据!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceService.remove(datasourceId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的数据模型列表。 + * + * @param onlineDatasourceDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDatasourceDto onlineDatasourceDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDatasource onlineDatasourceFilter = MyModelUtil.copyTo(onlineDatasourceDtoFilter, OnlineDatasource.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDatasource.class); + List onlineDatasourceList = + onlineDatasourceService.getOnlineDatasourceListWithRelation(onlineDatasourceFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDatasourceList, OnlineDatasourceVo.class)); + } + + /** + * 查看指定数据模型对象详情。 + * + * @param datasourceId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long datasourceId) { + ResponseResult verifyResult = this.doVerifyAndGet(datasourceId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasource onlineDatasource = + onlineDatasourceService.getByIdWithRelation(datasourceId, MyRelationParam.full()); + OnlineDatasourceVo onlineDatasourceVo = MyModelUtil.copyTo(onlineDatasource, OnlineDatasourceVo.class); + List tableList = onlineTableService.getOnlineTableListByDatasourceId(datasourceId); + if (CollUtil.isNotEmpty(tableList)) { + onlineDatasourceVo.setTableList(MyModelUtil.copyCollectionTo(tableList, OnlineTableVo.class)); + } + return ResponseResult.success(onlineDatasourceVo); + } + + private ResponseResult doVerifyAndGet(Long datasourceId) { + if (MyCommonUtil.existBlankArgument(datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDatasource onlineDatasource = onlineDatasourceService.getById(datasourceId); + if (onlineDatasource == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(onlineDatasource.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用并不存在该数据源!"); + } + return ResponseResult.success(onlineDatasource); + } + + private ResponseResult doVerifyPrimaryKey(SqlTable sqlTable, OnlinePage onlinePage) { + String errorMessage; + boolean hasPrimaryKey = false; + for (SqlTableColumn tableColumn : sqlTable.getColumnList()) { + if (BooleanUtil.isFalse(tableColumn.getPrimaryKey())) { + continue; + } + if (hasPrimaryKey) { + errorMessage = "数据验证失败,数据表只能包含一个主键字段!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + hasPrimaryKey = true; + // 流程表单的主表主键,不能是自增主键。 + if (BooleanUtil.isTrue(tableColumn.getAutoIncrement()) + && onlinePage.getPageType().equals(PageType.FLOW)) { + errorMessage = "数据验证失败,流程页面所关联的主表主键,不能是自增主键!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult verifyResult = onlineColumnService.verifyPrimaryKey(tableColumn); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + } + if (!hasPrimaryKey) { + errorMessage = "数据验证失败,数据表必须包含主键字段!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java new file mode 100644 index 00000000..31755e57 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDatasourceRelationController.java @@ -0,0 +1,260 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDatasourceRelationDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineDatasourceRelationVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单数据源关联接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据源关联接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDatasourceRelation") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDatasourceRelationController { + + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineFormService onlineFormService; + + /** + * 新增数据关联数据。 + * + * @param onlineDatasourceRelationDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineDatasourceRelationDto.relationId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDto) { + String errorMessage = MyCommonUtil.getModelValidationError( + onlineDatasourceRelationDto, Default.class, AddGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasourceRelation onlineDatasourceRelation = + MyModelUtil.copyTo(onlineDatasourceRelationDto, OnlineDatasourceRelation.class); + OnlineDatasource onlineDatasource = + onlineDatasourceService.getById(onlineDatasourceRelationDto.getDatasourceId()); + if (onlineDatasource == null) { + errorMessage = "数据验证失败,关联的数据源Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + if (!StrUtil.equals(onlineDatasource.getAppCode(), appCode)) { + errorMessage = "数据验证失败,当前应用并不包含该数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDblink onlineDblink = onlineDblinkService.getById(onlineDatasource.getDblinkId()); + SqlTable slaveTable = onlineDblinkService.getDblinkTable( + onlineDblink, onlineDatasourceRelationDto.getSlaveTableName()); + if (slaveTable == null) { + errorMessage = "数据验证失败,指定的数据表不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + SqlTableColumn slaveColumn = null; + for (SqlTableColumn column : slaveTable.getColumnList()) { + if (column.getColumnName().equals(onlineDatasourceRelationDto.getSlaveColumnName())) { + slaveColumn = column; + break; + } + } + if (slaveColumn == null) { + errorMessage = "数据验证失败,指定的数据表字段 [" + onlineDatasourceRelationDto.getSlaveColumnName() + "] 不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = + onlineDatasourceRelationService.verifyRelatedData(onlineDatasourceRelation, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + onlineDatasourceRelation = onlineDatasourceRelationService.saveNew(onlineDatasourceRelation, slaveTable, slaveColumn); + return ResponseResult.success(onlineDatasourceRelation.getRelationId()); + } + + /** + * 更新数据关联数据。 + * + * @param onlineDatasourceRelationDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDto) { + String errorMessage = MyCommonUtil.getModelValidationError( + onlineDatasourceRelationDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDatasourceRelation onlineDatasourceRelation = + MyModelUtil.copyTo(onlineDatasourceRelationDto, OnlineDatasourceRelation.class); + ResponseResult verifyResult = + this.doVerifyAndGet(onlineDatasourceRelation.getRelationId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation originalOnlineDatasourceRelation = verifyResult.getData(); + if (!onlineDatasourceRelationDto.getRelationType().equals(originalOnlineDatasourceRelation.getRelationType())) { + errorMessage = "数据验证失败,不能修改关联类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceRelationDto.getSlaveTableId().equals(originalOnlineDatasourceRelation.getSlaveTableId())) { + errorMessage = "数据验证失败,不能修改从表Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceRelationDto.getDatasourceId().equals(originalOnlineDatasourceRelation.getDatasourceId())) { + errorMessage = "数据验证失败,不能修改数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = onlineDatasourceRelationService + .verifyRelatedData(onlineDatasourceRelation, originalOnlineDatasourceRelation); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!onlineDatasourceRelationService.update(onlineDatasourceRelation, originalOnlineDatasourceRelation)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除数据关联数据。 + * + * @param relationId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long relationId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation onlineDatasourceRelation = verifyResult.getData(); + OnlineVirtualColumn virtualColumnFilter = new OnlineVirtualColumn(); + virtualColumnFilter.setRelationId(relationId); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnList(virtualColumnFilter, null); + if (CollUtil.isNotEmpty(virtualColumnList)) { + OnlineVirtualColumn virtualColumn = virtualColumnList.get(0); + errorMessage = "数据验证失败,数据源关联正在被虚拟字段 [" + virtualColumn.getColumnPrompt() + "] 使用,不能被删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + List formList = + onlineFormService.getOnlineFormListByTableId(onlineDatasourceRelation.getSlaveTableId()); + if (CollUtil.isNotEmpty(formList)) { + errorMessage = "数据验证失败,当前数据源关联正在被 [" + formList.get(0).getFormName() + "] 表单占用,请先删除关联数据!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineDatasourceRelationService.remove(relationId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的数据关联列表。 + * + * @param onlineDatasourceRelationDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分 页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDatasourceRelation onlineDatasourceRelationFilter = + MyModelUtil.copyTo(onlineDatasourceRelationDtoFilter, OnlineDatasourceRelation.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDatasourceRelation.class); + List onlineDatasourceRelationList = + onlineDatasourceRelationService.getOnlineDatasourceRelationListWithRelation(onlineDatasourceRelationFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDatasourceRelationList, OnlineDatasourceRelationVo.class)); + } + + /** + * 查看指定数据关联对象详情。 + * + * @param relationId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long relationId) { + ResponseResult verifyResult = this.doVerifyAndGet(relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation onlineDatasourceRelation = + onlineDatasourceRelationService.getByIdWithRelation(relationId, MyRelationParam.full()); + return ResponseResult.success(onlineDatasourceRelation, OnlineDatasourceRelationVo.class); + } + + private ResponseResult doVerifyAndGet(Long relationId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(relationId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDatasourceRelation relation = + onlineDatasourceRelationService.getByIdWithRelation(relationId, MyRelationParam.full()); + if (relation == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(relation.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该数据源关联!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(relation); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java new file mode 100644 index 00000000..60447f1e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDblinkController.java @@ -0,0 +1,276 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDblinkDto; +import com.orangeforms.common.online.model.OnlineDblink; +import com.orangeforms.common.online.service.OnlineDblinkService; +import com.orangeforms.common.online.util.OnlineDataSourceUtil; +import com.orangeforms.common.online.vo.OnlineDblinkVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 在线表单数据库链接接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据库链接接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDblink") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDblinkController { + + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + /** + * 新增数据库链接数据。 + * + * @param onlineDblinkDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @SaCheckPermission("onlineDblink.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineDblinkDto onlineDblinkDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDblinkDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDblink onlineDblink = MyModelUtil.copyTo(onlineDblinkDto, OnlineDblink.class); + onlineDblink = onlineDblinkService.saveNew(onlineDblink); + return ResponseResult.success(onlineDblink.getDblinkId()); + } + + /** + * 更新数据库链接数据。 + * + * @param onlineDblinkDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDblink.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDblinkDto onlineDblinkDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDblinkDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDblink onlineDblink = MyModelUtil.copyTo(onlineDblinkDto, OnlineDblink.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineDblinkDto.getDblinkId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDblink originalOnlineDblink = verifyResult.getData(); + if (ObjectUtil.notEqual(onlineDblink.getDblinkType(), originalOnlineDblink.getDblinkType())) { + errorMessage = "数据验证失败,不能修改数据库类型!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + String passwdKey = "password"; + JSONObject configJson = JSON.parseObject(onlineDblink.getConfiguration()); + String password = configJson.getString(passwdKey); + if (StrUtil.isNotBlank(password) && StrUtil.isAllCharMatch(password, c -> '*' == c)) { + password = JSON.parseObject(originalOnlineDblink.getConfiguration()).getString(passwdKey); + configJson.put(passwdKey, password); + onlineDblink.setConfiguration(configJson.toJSONString()); + } + if (!onlineDblinkService.update(onlineDblink, originalOnlineDblink)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除数据库链接数据。 + * + * @param dblinkId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDblink.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long dblinkId) { + String errorMessage; + // 验证关联Id的数据合法性 + ResponseResult verifyResult = this.doVerifyAndGet(dblinkId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineDblinkService.remove(dblinkId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的数据库链接列表。 + * + * @param onlineDblinkDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlineDblink.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDblinkDto onlineDblinkDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDblink onlineDblinkFilter = MyModelUtil.copyTo(onlineDblinkDtoFilter, OnlineDblink.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDblink.class); + List onlineDblinkList = + onlineDblinkService.getOnlineDblinkListWithRelation(onlineDblinkFilter, orderBy); + for (OnlineDblink dblink : onlineDblinkList) { + this.maskOffPassword(dblink); + } + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDblinkList, OnlineDblinkVo.class)); + } + + /** + * 查看指定数据库链接对象详情。 + * + * @param dblinkId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlineDblink.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long dblinkId) { + ResponseResult verifyResult = this.doVerifyAndGet(dblinkId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDblink onlineDblink = verifyResult.getData(); + onlineDblinkService.buildRelationForData(onlineDblink, MyRelationParam.full()); + if (!StrUtil.equals(onlineDblink.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用并不存在该数据库链接!"); + } + this.maskOffPassword(onlineDblink); + return ResponseResult.success(onlineDblink, OnlineDblinkVo.class); + } + + /** + * 获取指定数据库链接下的所有动态表单依赖的数据表列表。 + * + * @param dblinkId 数据库链接Id。 + * @return 所有动态表单依赖的数据表列表 + */ + @SaCheckPermission("onlineDblink.all") + @GetMapping("/listDblinkTables") + public ResponseResult> listDblinkTables(@RequestParam Long dblinkId) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineDblinkService.getDblinkTableList(dblink)); + } + + /** + * 获取指定数据库链接下,指定数据表的所有字段信息。 + * + * @param dblinkId 数据库链接Id。 + * @param tableName 表名。 + * @return 该表的所有字段列表。 + */ + @SaCheckPermission("onlineDblink.all") + @GetMapping("/listDblinkTableColumns") + public ResponseResult> listDblinkTableColumns( + @RequestParam Long dblinkId, @RequestParam String tableName) { + OnlineDblink dblink = onlineDblinkService.getById(dblinkId); + if (dblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineDblinkService.getDblinkTableColumnList(dblink, tableName)); + } + + /** + * 测试数据库链接的接口。 + * + * @return 应答结果。 + */ + @GetMapping("/testConnection") + public ResponseResult testConnection(@RequestParam Long dblinkId) { + ResponseResult verifyAndGet = this.doVerifyAndGet(dblinkId); + if (!verifyAndGet.isSuccess()) { + return ResponseResult.errorFrom(verifyAndGet); + } + try { + dataSourceUtil.testConnection(dblinkId); + return ResponseResult.success(); + } catch (Exception e) { + log.error("Failed to test connection with ONLINE_DBLINK_ID [" + dblinkId + "]!", e); + return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "数据库连接失败!"); + } + } + + /** + * 以字典形式返回全部数据库链接数据集合。字典的键值为[dblinkId, dblinkName]。 + * 白名单接口,登录用户均可访问。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含的数据为 List>,map中包含两条记录,key的值分别是id和name,value对应具体数据。 + */ + @GetMapping("/listDict") + public ResponseResult>> listDict(@ParameterObject OnlineDblinkDto filter) { + List resultList = + onlineDblinkService.getOnlineDblinkList(MyModelUtil.copyTo(filter, OnlineDblink.class), null); + return ResponseResult.success( + MyCommonUtil.toDictDataList(resultList, OnlineDblink::getDblinkId, OnlineDblink::getDblinkName)); + } + + private ResponseResult doVerifyAndGet(Long dblinkId) { + if (MyCommonUtil.existBlankArgument(dblinkId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDblink onlineDblink = onlineDblinkService.getById(dblinkId); + if (onlineDblink == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(onlineDblink.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error( + ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用并不存在该数据库链接!"); + } + return ResponseResult.success(onlineDblink); + } + + private void maskOffPassword(OnlineDblink dblink) { + String passwdKey = "password"; + JSONObject configJson = JSON.parseObject(dblink.getConfiguration()); + if (configJson.containsKey(passwdKey)) { + String password = configJson.getString(passwdKey); + if (StrUtil.isNotBlank(password)) { + configJson.put(passwdKey, StrUtil.repeat('*', password.length())); + dblink.setConfiguration(configJson.toJSONString()); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java new file mode 100644 index 00000000..3b31c21b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineDictController.java @@ -0,0 +1,221 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.dict.dto.GlobalDictDto; +import com.orangeforms.common.dict.util.GlobalDictOperationHelper; +import com.orangeforms.common.dict.vo.GlobalDictVo; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDictDto; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDict; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.service.OnlineDictService; +import com.orangeforms.common.online.vo.OnlineDictVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单字典接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单字典接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineDict") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineDictController { + + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private GlobalDictOperationHelper globalDictOperationHelper; + + /** + * 新增在线表单字典数据。 + * + * @param onlineDictDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineDictDto.dictId"}) + @SaCheckPermission("onlineDict.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineDictDto onlineDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDictDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDict onlineDict = MyModelUtil.copyTo(onlineDictDto, OnlineDict.class); + // 验证关联Id的数据合法性 + CallResult callResult = onlineDictService.verifyRelatedData(onlineDict, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + onlineDict = onlineDictService.saveNew(onlineDict); + return ResponseResult.success(onlineDict.getDictId()); + } + + /** + * 更新在线表单字典数据。 + * + * @param onlineDictDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDict.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineDictDto onlineDictDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineDictDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineDict onlineDict = MyModelUtil.copyTo(onlineDictDto, OnlineDict.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineDict.getDictId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDict originalOnlineDict = verifyResult.getData(); + // 验证关联Id的数据合法性 + CallResult callResult = onlineDictService.verifyRelatedData(onlineDict, originalOnlineDict); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!onlineDictService.update(onlineDict, originalOnlineDict)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除在线表单字典数据。 + * + * @param dictId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlineDict.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long dictId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(dictId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineColumn filter = new OnlineColumn(); + filter.setDictId(dictId); + List columns = onlineColumnService.getListByFilter(filter); + if (CollUtil.isNotEmpty(columns)) { + OnlineColumn usingColumn = columns.get(0); + OnlineTable table = onlineTableService.getById(usingColumn.getTableId()); + errorMessage = String.format("数据验证失败,数据表 [%s] 字段 [%s] 正在引用该字典,因此不能直接删除!", + table.getTableName(), usingColumn.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!onlineDictService.remove(dictId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的在线表单字典列表。 + * + * @param onlineDictDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlineDict.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineDictDto onlineDictDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDict onlineDictFilter = MyModelUtil.copyTo(onlineDictDtoFilter, OnlineDict.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDict.class); + List onlineDictList = onlineDictService.getOnlineDictListWithRelation(onlineDictFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDictList, OnlineDictVo.class)); + } + + /** + * 查看指定在线表单字典对象详情。 + * + * @param dictId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlineDict.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long dictId) { + ResponseResult verifyResult = this.doVerifyAndGet(dictId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDict onlineDict = onlineDictService.getByIdWithRelation(dictId, MyRelationParam.full()); + if (onlineDict == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineDict, OnlineDictVo.class); + } + + /** + * 获取全部编码字典列表。 + * NOTE: 白名单接口。 + * + * @param globalDictDtoFilter 过滤对象。 + * @param pageParam 分页参数。 + * @return 字典的数据列表。 + */ + @PostMapping("/listAllGlobalDict") + public ResponseResult> listAllGlobalDict( + @MyRequestBody GlobalDictDto globalDictDtoFilter, + @MyRequestBody MyPageParam pageParam) { + return globalDictOperationHelper.listAllGlobalDict(globalDictDtoFilter, pageParam); + } + + private ResponseResult doVerifyAndGet(Long dictId) { + if (MyCommonUtil.existBlankArgument(dictId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineDict originalDict = onlineDictService.getById(dictId); + if (originalDict == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(originalDict.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + return ResponseResult.error( + ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,当前应用不存在该在线表单字典!"); + } + return ResponseResult.success(originalDict); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java new file mode 100644 index 00000000..921ffee7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineFormController.java @@ -0,0 +1,428 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dto.OnlineFormDto; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.vo.OnlineFormVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.validation.groups.Default; +import java.util.HashSet; +import java.util.List; +import java.util.LinkedList; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 在线表单表单接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单表单接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineForm") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineFormController { + + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineRuleService onlineRuleService; + @Autowired + private OnlineProperties properties; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + /** + * 新增在线表单数据。 + * + * @param onlineFormDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineFormDto.formId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineFormDto onlineFormDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineFormDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineForm onlineForm = MyModelUtil.copyTo(onlineFormDto, OnlineForm.class); + if (onlineFormService.existByFormCode(onlineForm.getFormCode())) { + errorMessage = "数据验证失败,表单编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + // 验证关联Id的数据合法性 + CallResult callResult = onlineFormService.verifyRelatedData(onlineForm, null); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + Set datasourceIdSet = null; + if (CollUtil.isNotEmpty(onlineFormDto.getDatasourceIdList())) { + ResponseResult> verifyDatasourceIdsResult = + this.doVerifyDatasourceIdsAndGet(onlineFormDto.getDatasourceIdList()); + if (!verifyDatasourceIdsResult.isSuccess()) { + return ResponseResult.errorFrom(verifyDatasourceIdsResult); + } + datasourceIdSet = verifyDatasourceIdsResult.getData(); + } + onlineForm = onlineFormService.saveNew(onlineForm, datasourceIdSet); + return ResponseResult.success(onlineForm.getFormId()); + } + + /** + * 更新在线表单数据。 + * + * @param onlineFormDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineFormDto onlineFormDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineFormDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineForm onlineForm = MyModelUtil.copyTo(onlineFormDto, OnlineForm.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineForm.getFormId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineForm originalOnlineForm = verifyResult.getData(); + // 验证关联Id的数据合法性 + CallResult callResult = onlineFormService.verifyRelatedData(onlineForm, originalOnlineForm); + if (!callResult.isSuccess()) { + errorMessage = callResult.getErrorMessage(); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(onlineForm.getFormCode(), originalOnlineForm.getFormCode()) + && onlineFormService.existByFormCode(onlineForm.getFormCode())) { + errorMessage = "数据验证失败,表单编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + Set datasourceIdSet = null; + if (CollUtil.isNotEmpty(onlineFormDto.getDatasourceIdList())) { + ResponseResult> verifyDatasourceIdsResult = + this.doVerifyDatasourceIdsAndGet(onlineFormDto.getDatasourceIdList()); + if (!verifyDatasourceIdsResult.isSuccess()) { + return ResponseResult.errorFrom(verifyDatasourceIdsResult); + } + datasourceIdSet = verifyDatasourceIdsResult.getData(); + } + if (!onlineFormService.update(onlineForm, originalOnlineForm, datasourceIdSet)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除在线表单数据。 + * + * @param formId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long formId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(formId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineFormService.remove(formId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 克隆一个在线表单对象。 + * + * @param formId 源表单主键Id。 + * @return 新克隆表单主键Id。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/clone") + public ResponseResult clone(@MyRequestBody Long formId) { + ResponseResult verifyResult = this.doVerifyAndGet(formId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineForm form = verifyResult.getData(); + form.setFormName(form.getFormName() + "_copy"); + form.setFormCode(form.getFormCode() + "_copy_" + System.currentTimeMillis()); + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + Set datasourceIdSet = formDatasourceList.stream() + .map(OnlineFormDatasource::getDatasourceId).collect(Collectors.toSet()); + onlineFormService.saveNew(form, datasourceIdSet); + return ResponseResult.success(form.getFormId()); + } + + /** + * 列出符合过滤条件的在线表单列表。 + * + * @param onlineFormDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineFormDto onlineFormDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineForm onlineFormFilter = MyModelUtil.copyTo(onlineFormDtoFilter, OnlineForm.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineForm.class); + List onlineFormList = + onlineFormService.getOnlineFormListWithRelation(onlineFormFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineFormList, OnlineFormVo.class)); + } + + /** + * 查看指定在线表单对象详情。 + * + * @param formId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long formId) { + ResponseResult verifyResult = this.doVerifyAndGet(formId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineForm onlineForm = onlineFormService.getByIdWithRelation(formId, MyRelationParam.full()); + OnlineFormVo onlineFormVo = MyModelUtil.copyTo(onlineForm, OnlineFormVo.class); + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + if (CollUtil.isNotEmpty(formDatasourceList)) { + onlineFormVo.setDatasourceIdList(formDatasourceList.stream() + .map(OnlineFormDatasource::getDatasourceId).collect(Collectors.toList())); + } + return ResponseResult.success(onlineFormVo); + } + + /** + * 获取指定在线表单对象在前端渲染时所需的所有数据对象。 + * + * @param formId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @GetMapping("/render") + public ResponseResult render(@RequestParam Long formId) { + String errorMessage; + Cache cache = null; + if (BooleanUtil.isTrue(properties.getEnableRenderCache())) { + cache = cacheManager.getCache(CacheConfig.CacheEnum.ONLINE_FORM_RENDER_CACCHE.name()); + Assert.notNull(cache, "Cache ONLINE_FORM_RENDER_CACCHE can't be NULL"); + JSONObject responseData = cache.get(formId, JSONObject.class); + if (responseData != null) { + Object appCode = responseData.get("appCode"); + if (ObjectUtil.notEqual(appCode, TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该表单Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(responseData); + } + } + OnlineForm onlineForm = onlineFormService.getOnlineFormFromCache(formId); + if (onlineForm == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + OnlineFormVo onlineFormVo = MyModelUtil.copyTo(onlineForm, OnlineFormVo.class); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("onlineForm", onlineFormVo); + List formDatasourceList = onlineFormService.getFormDatasourceListFromCache(formId); + if (CollUtil.isEmpty(formDatasourceList)) { + return ResponseResult.success(jsonObject); + } + Set datasourceIdSet = formDatasourceList.stream() + .map(OnlineFormDatasource::getDatasourceId).collect(Collectors.toSet()); + List onlineDatasourceList = + onlineDatasourceService.getOnlineDatasourceListFromCache(datasourceIdSet); + jsonObject.put("onlineDatasourceList", onlineDatasourceList); + Set tableIdSet = onlineDatasourceList.stream() + .map(OnlineDatasource::getMasterTableId).collect(Collectors.toSet()); + List onlineDatasourceRelationList = + onlineDatasourceRelationService.getOnlineDatasourceRelationListFromCache(datasourceIdSet); + if (CollUtil.isNotEmpty(onlineDatasourceRelationList)) { + jsonObject.put("onlineDatasourceRelationList", onlineDatasourceRelationList); + tableIdSet.addAll(onlineDatasourceRelationList.stream() + .map(OnlineDatasourceRelation::getSlaveTableId).collect(Collectors.toList())); + } + List onlineTableList = new LinkedList<>(); + List onlineColumnList = new LinkedList<>(); + for (Long tableId : tableIdSet) { + OnlineTable table = onlineTableService.getOnlineTableFromCache(tableId); + onlineTableList.add(table); + onlineColumnList.addAll(table.getColumnMap().values()); + table.setColumnMap(null); + } + jsonObject.put("onlineTableList", onlineTableList); + jsonObject.put("onlineColumnList", onlineColumnList); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnListByTableIds(tableIdSet); + jsonObject.put("onlineVirtualColumnList", virtualColumnList); + Set dictIdSet = onlineColumnList.stream() + .filter(c -> c.getDictId() != null).map(OnlineColumn::getDictId).collect(Collectors.toSet()); + Set widgetDictIdSet = this.extractDictIdSetFromWidgetJson(onlineForm.getWidgetJson()); + CollUtil.addAll(dictIdSet, widgetDictIdSet); + if (CollUtil.isNotEmpty(dictIdSet)) { + List onlineDictList = onlineDictService.getOnlineDictListFromCache(dictIdSet); + if (onlineDictList.size() != dictIdSet.size()) { + Set columnDictIdSet = onlineDictList.stream().map(OnlineDict::getDictId).collect(Collectors.toSet()); + Long notExistDictId = this.findNotExistDictId(dictIdSet, columnDictIdSet); + Assert.notNull(notExistDictId, "notExistDictId can't be NULL"); + errorMessage = String.format("数据验证失败,字典Id [%s] 不存在!", notExistDictId); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + jsonObject.put("onlineDictList", onlineDictList); + } + Set columnIdSet = onlineColumnList.stream().map(OnlineColumn::getColumnId).collect(Collectors.toSet()); + List colunmRuleList = onlineRuleService.getOnlineColumnRuleListByColumnIds(columnIdSet); + if (CollUtil.isNotEmpty(colunmRuleList)) { + jsonObject.put("onlineColumnRuleList", colunmRuleList); + } + jsonObject.put("appCode", TokenData.takeFromRequest().getAppCode()); + if (BooleanUtil.isTrue(properties.getEnableRenderCache())) { + Assert.notNull(cache, "Cache ONLINE_FORM_RENDER_CACCHE can't be NULL"); + cache.put(formId, jsonObject); + } + return ResponseResult.success(jsonObject); + } + + private Long findNotExistDictId(Set originalDictIdSet, Set dictIdSet) { + return originalDictIdSet.stream().filter(d -> !dictIdSet.contains(d)).findFirst().orElse(null); + } + + private ResponseResult doVerifyAndGet(Long formId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(formId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineForm form = onlineFormService.getById(formId); + if (form == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(form.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该表单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(form.getTenantId(), TokenData.takeFromRequest().getTenantId())) { + errorMessage = "数据验证失败,当前租户不包含该表单!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(form); + } + + private ResponseResult> doVerifyDatasourceIdsAndGet(List datasourceIdList) { + String errorMessage; + Set datasourceIdSet = new HashSet<>(datasourceIdList); + List datasourceList = onlineDatasourceService.getInList(datasourceIdSet); + if (datasourceIdSet.size() != datasourceList.size()) { + errorMessage = "数据验证失败,当前在线表单包含不存在的数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + for (OnlineDatasource datasource : datasourceList) { + if (!StrUtil.equals(datasource.getAppCode(), appCode)) { + errorMessage = "数据验证失败,存在不是当前应用的数据源!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(datasourceIdSet); + } + + private Set extractDictIdSetFromWidgetJson(String widgetJson) { + Set dictIdSet = new HashSet<>(); + if (StrUtil.isBlank(widgetJson)) { + return dictIdSet; + } + JSONObject allData = JSON.parseObject(widgetJson); + JSONObject pcData = allData.getJSONObject("pc"); + if (MapUtil.isEmpty(pcData)) { + return dictIdSet; + } + JSONArray widgetListArray = pcData.getJSONArray("widgetList"); + if (CollUtil.isEmpty(widgetListArray)) { + return dictIdSet; + } + for (int i = 0; i < widgetListArray.size(); i++) { + this.recursiveExtractDictId(widgetListArray.getJSONObject(i), dictIdSet); + } + return dictIdSet; + } + + private void recursiveExtractDictId(JSONObject widgetData, Set dictIdSet) { + JSONObject propsData = widgetData.getJSONObject("props"); + if (MapUtil.isNotEmpty(propsData)) { + JSONObject dictInfoData = propsData.getJSONObject("dictInfo"); + if (MapUtil.isNotEmpty(dictInfoData)) { + Long dictId = dictInfoData.getLong("dictId"); + if (dictId != null) { + dictIdSet.add(dictId); + } + } + } + JSONArray childWidgetArray = widgetData.getJSONArray("childWidgetList"); + if (CollUtil.isNotEmpty(childWidgetArray)) { + for (int i = 0; i < childWidgetArray.size(); i++) { + this.recursiveExtractDictId(childWidgetArray.getJSONObject(i), dictIdSet); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java new file mode 100644 index 00000000..92638650 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineOperationController.java @@ -0,0 +1,1045 @@ +package com.orangeforms.common.online.controller; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.DictType; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.dict.model.GlobalDictItem; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.exception.OnlineRuntimeException; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.util.OnlineConstant; +import com.orangeforms.common.online.util.OnlineOperationHelper; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线表单数据操作接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单数据操作接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineOperation") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineOperationController { + + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineOperationHelper onlineOperationHelper; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private GlobalDictService globalDictService; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private SessionCacheHelper sessionCacheHelper; + + /** + * 新增数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表的数据源Id。 + * @param masterData 主表新增数据。 + * @param slaveData 一对多从表新增数据列表。 + * @return 应答结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/addDatasource/{datasourceVariableName}") + public ResponseResult addDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData) { + // 验证数据源的合法性,同时获取主表对象。 + ResponseResult datasourceResult = onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + OnlineTable masterTable = datasource.getMasterTable(); + if (slaveData == null) { + onlineOperationService.saveNew(masterTable, masterData); + } else { + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasourceId, slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + onlineOperationService.saveNewWithRelation(masterTable, masterData, slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + /** + * 新增一对多从表数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表的数据源Id。 + * @param relationId 一对多的关联Id。 + * @param slaveData 一对多从表的新增数据列表。 + * @return 应答结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/addOneToManyRelation/{datasourceVariableName}") + public ResponseResult addOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) JSONObject slaveData) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation relation = verifyResult.getData(); + onlineOperationService.saveNew(relation.getSlaveTable(), slaveData); + return ResponseResult.success(); + } + + /** + * 更新主数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param masterData 表数据。这里没有包含的字段将视为NULL。 + * @param slaveData 从表数据,key是relationId。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateDatasource/{datasourceVariableName}") + public ResponseResult updateDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) JSONObject masterData, + @MyRequestBody JSONObject slaveData) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + OnlineTable masterTable = datasource.getMasterTable(); + if (slaveData == null) { + if (!onlineOperationService.update(masterTable, masterData)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } else { + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasourceId, slaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + onlineOperationService.updateWithRelation( + masterTable, masterData, datasourceId, slaveDataListResult.getData()); + } + return ResponseResult.success(); + } + + /** + * 更新一对多关联数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param relationId 一对多关联Id。 + * @param slaveData 一对多关联从表数据。这里没有包含的字段将视为NULL。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updateOneToManyRelation/{datasourceVariableName}") + public ResponseResult updateOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) JSONObject slaveData) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineTable slaveTable = verifyResult.getData().getSlaveTable(); + if (!onlineOperationService.update(slaveTable, slaveData)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除主数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param dataId 待删除的数据表主键Id。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/deleteDatasource/{datasourceVariableName}") + public ResponseResult deleteDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) String dataId) { + return this.doDelete(datasourceVariableName, datasourceId, CollUtil.newArrayList(dataId)); + } + + /** + * 批量删除主数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param dataIdList 待删除的数据表主键Id列表。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatchDatasource/{datasourceVariableName}") + public ResponseResult deleteBatchDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) List dataIdList) { + return this.doDelete(datasourceVariableName, datasourceId, dataIdList); + } + + /** + * 删除一对多关联表单条数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param relationId 一对多关联Id。 + * @param dataId 一对多关联表主键Id。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/deleteOneToManyRelation/{datasourceVariableName}") + public ResponseResult deleteOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) String dataId) { + return this.doDelete(datasourceVariableName, datasourceId, relationId, CollUtil.newArrayList(dataId)); + } + + /** + * 批量删除一对多关联表单条数据接口。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 主表数据源Id。 + * @param relationId 一对多关联Id。 + * @param dataIdList 一对多关联表主键Id列表。 + * @return 应该结果。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DELETE_BATCH) + @PostMapping("/deleteBatchOneToManyRelation/{datasourceVariableName}") + public ResponseResult deleteBatchOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody(required = true) List dataIdList) { + return this.doDelete(datasourceVariableName, datasourceId, relationId, dataIdList); + } + + /** + * 根据数据源Id为动态表单查询数据详情。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param dataId 数据主键Id。 + * @return 详情结果。 + */ + @SaTokenDenyAuth + @GetMapping("/viewByDatasourceId/{datasourceVariableName}") + public ResponseResult> viewByDatasourceId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam String dataId) { + // 验证数据源及其关联 + ResponseResult datasourceResult = + this.doVerifyAndGetDatasource(datasourceId, datasourceVariableName); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + List allRelationList = relationListResult.getData(); + List oneToOneRelationList = allRelationList.stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + Map result = onlineOperationService.getMasterData( + datasource.getMasterTable(), oneToOneRelationList, allRelationList, dataId); + return ResponseResult.success(result); + } + + /** + * 根据数据源关联Id为动态表单查询数据详情。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 一对多关联Id。 + * @param dataId 一对多关联数据主键Id。 + * @return 详情结果。 + */ + @SaTokenDenyAuth + @GetMapping("/viewByOneToManyRelationId/{datasourceVariableName}") + public ResponseResult> viewByOneToManyRelationId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam Long relationId, + @RequestParam String dataId) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + Map result = onlineOperationService.getSlaveData(verifyResult.getData(), dataId); + return ResponseResult.success(result); + } + + /** + * 为数据源主表字段下载文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param dataId 附件所在记录的主键Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param response Http 应答对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/downloadDatasource/{datasourceVariableName}") + public void downloadDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam(required = false) String dataId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) throws IOException { + if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(datasourceResult)); + return; + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return; + } + OnlineTable masterTable = datasource.getMasterTable(); + onlineOperationHelper.doDownload(masterTable, dataId, fieldName, filename, asImage, response); + } + + /** + * 为数据源一对多关联的从表字段下载文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param dataId 附件所在记录的主键Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param response Http 应答对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false) + @GetMapping("/downloadOneToManyRelation/{datasourceVariableName}") + public void downloadOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam Long relationId, + @RequestParam(required = false) String dataId, + @RequestParam String fieldName, + @RequestParam String filename, + @RequestParam Boolean asImage, + HttpServletResponse response) throws IOException { + ResponseResult relationResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!relationResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(relationResult)); + return; + } + OnlineTable slaveTable = relationResult.getData().getSlaveTable(); + onlineOperationHelper.doDownload(slaveTable, dataId, fieldName, filename, asImage, response); + } + + /** + * 为数据源主表字段上传文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param uploadFile 上传文件对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/uploadDatasource/{datasourceVariableName}") + public void uploadDatasource( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(datasourceResult)); + return; + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return; + } + OnlineTable masterTable = datasource.getMasterTable(); + onlineOperationHelper.doUpload(masterTable, fieldName, asImage, uploadFile); + } + + /** + * 为数据源一对多关联的从表字段上传文件。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片文件。 + * @param uploadFile 上传文件对象。 + */ + @SaTokenDenyAuth + @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false) + @PostMapping("/uploadOneToManyRelation/{datasourceVariableName}") + public void uploadOneToManyRelation( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @RequestParam Long datasourceId, + @RequestParam Long relationId, + @RequestParam String fieldName, + @RequestParam Boolean asImage, + @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException { + ResponseResult relationResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!relationResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.errorFrom(relationResult)); + return; + } + OnlineTable slaveTable = relationResult.getData().getSlaveTable(); + onlineOperationHelper.doUpload(slaveTable, fieldName, asImage, uploadFile); + } + + /** + * 根据数据源Id,以及接口参数,为动态表单查询数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param pageParam 分页对象。 + */ + @SaTokenDenyAuth + @PostMapping("/listByDatasourceId/{datasourceVariableName}") + public ResponseResult>> listByDatasourceId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + // 1. 验证数据源及其关联 + ResponseResult datasourceResult = + this.doVerifyAndGetDatasource(datasourceId, datasourceVariableName); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineTable masterTable = datasourceResult.getData().getMasterTable(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, null); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + List allRelationList = relationListResult.getData(); + // 2. 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + return ResponseResult.errorFrom(filterDtoListResult); + } + // 3. 解析排序参数,同时确保没有sql注入。 + Map tableMap = new HashMap<>(4); + tableMap.put(masterTable.getTableName(), masterTable); + List oneToOneRelationList = relationListResult.getData().stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(oneToOneRelationList)) { + Map relationTableMap = oneToOneRelationList.stream() + .map(OnlineDatasourceRelation::getSlaveTable).collect(Collectors.toMap(OnlineTable::getTableName, c -> c)); + tableMap.putAll(relationTableMap); + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, masterTable, tableMap); + if (!orderByResult.isSuccess()) { + return ResponseResult.errorFrom(orderByResult); + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = onlineOperationService.getMasterDataList( + masterTable, oneToOneRelationList, allRelationList, filterDtoList, orderBy, pageParam); + return ResponseResult.success(pageData); + } + + /** + * 根据数据源Id,以及接口参数,为动态表单导出数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param exportInfoList 导出字段信息列表。 + */ + @SaTokenDenyAuth + @PostMapping("/exportByDatasourceId/{datasourceVariableName}") + public void exportByDatasourceId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody(required = true) List exportInfoList) throws IOException { + // 1. 验证数据源及其关联 + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + } + OnlineTable masterTable = datasource.getMasterTable(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, null); + if (!relationListResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, relationListResult); + } + List allRelationList = relationListResult.getData(); + // 2. 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, filterDtoListResult); + } + // 3. 解析排序参数,同时确保没有sql注入。 + Map tableMap = new HashMap<>(4); + tableMap.put(masterTable.getTableName(), masterTable); + List oneToOneRelationList = relationListResult.getData().stream() + .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(oneToOneRelationList)) { + Map relationTableMap = oneToOneRelationList.stream() + .map(OnlineDatasourceRelation::getSlaveTable).collect(Collectors.toMap(OnlineTable::getTableName, c -> c)); + tableMap.putAll(relationTableMap); + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, masterTable, tableMap); + if (!orderByResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, orderByResult); + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = onlineOperationService.getMasterDataList( + masterTable, oneToOneRelationList, allRelationList, filterDtoList, orderBy, null); + Map headerMap = this.makeExportHeaderMap(masterTable, allRelationList, exportInfoList); + if (MapUtil.isEmpty(headerMap)) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, + ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,没有指定导出头信息!")); + return; + } + this.normalizeExportDataList(pageData.getDataList()); + String filename = datasourceVariableName + "-" + MyDateUtil.toDateTimeString(DateTime.now()) + ".xlsx"; + ExportUtil.doExport(pageData.getDataList(), headerMap, filename); + } + + /** + * 根据数据源Id和数据源关联Id,以及接口参数,为动态表单查询该一对多关联的数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param pageParam 分页对象。 + * @return 查询结果。 + */ + @SaTokenDenyAuth + @PostMapping("/listByOneToManyRelationId/{datasourceVariableName}") + public ResponseResult>> listByOneToManyRelationId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation relation = verifyResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + // 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + return ResponseResult.errorFrom(filterDtoListResult); + } + Map tableMap = new HashMap<>(1); + tableMap.put(slaveTable.getTableName(), slaveTable); + if (CollUtil.isNotEmpty(orderParam)) { + for (MyOrderParam.OrderInfo orderInfo : orderParam) { + orderInfo.setFieldName(StrUtil.removePrefix(orderInfo.getFieldName(), + relation.getVariableName() + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR)); + } + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, slaveTable, tableMap); + if (!orderByResult.isSuccess()) { + return ResponseResult.errorFrom(orderByResult); + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = + onlineOperationService.getSlaveDataList(relation, filterDtoList, orderBy, pageParam); + return ResponseResult.success(pageData); + } + + /** + * 根据数据源Id和数据源关联Id,以及接口参数,为动态表单查询该一对多关联的数据列表。 + * + * @param datasourceVariableName 数据源名称。 + * @param datasourceId 数据源Id。 + * @param relationId 数据源的一对多关联Id。 + * @param filterDtoList 多虑数据对象列表。 + * @param orderParam 排序对象。 + * @param exportInfoList 导出字段信息列表。 + */ + @SaTokenDenyAuth + @PostMapping("/exportByOneToManyRelationId/{datasourceVariableName}") + public void exportByOneToManyRelationId( + @PathVariable("datasourceVariableName") String datasourceVariableName, + @MyRequestBody(required = true) Long datasourceId, + @MyRequestBody(required = true) Long relationId, + @MyRequestBody List filterDtoList, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody(required = true) List exportInfoList) throws IOException { + ResponseResult relationResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!relationResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, relationResult); + return; + } + OnlineDatasourceRelation relation = relationResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + // 验证数据过滤对象中的表名和字段,确保没有sql注入。 + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, filterDtoListResult); + return; + } + Map tableMap = new HashMap<>(1); + tableMap.put(slaveTable.getTableName(), slaveTable); + if (CollUtil.isNotEmpty(orderParam)) { + for (MyOrderParam.OrderInfo orderInfo : orderParam) { + orderInfo.setFieldName(StrUtil.removePrefix(orderInfo.getFieldName(), + relation.getVariableName() + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR)); + } + } + ResponseResult orderByResult = this.makeOrderBy(orderParam, slaveTable, tableMap); + if (!orderByResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, orderByResult); + return; + } + String orderBy = orderByResult.getData(); + MyPageData> pageData = + onlineOperationService.getSlaveDataList(relation, filterDtoList, orderBy, null); + Map headerMap = + this.makeExportHeaderMap(relation.getSlaveTable(), null, exportInfoList); + if (MapUtil.isEmpty(headerMap)) { + ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST, + ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,没有指定导出头信息!")); + return; + } + this.normalizeExportDataList(pageData.getDataList()); + String filename = datasourceVariableName + "-relation-" + MyDateUtil.toDateTimeString(DateTime.now()) + ".xlsx"; + ExportUtil.doExport(pageData.getDataList(), headerMap, filename); + } + + /** + * 查询字典数据,并以字典的约定方式,返回数据结果集。 + * + * @param dictId 字典Id。 + * @param filterDtoList 字典的过滤对象列表。 + * @return 字典数据列表。 + */ + @PostMapping("/listDict") + public ResponseResult>> listDict( + @MyRequestBody(required = true) Long dictId, + @MyRequestBody List filterDtoList) { + String errorMessage; + OnlineDict dict = onlineDictService.getOnlineDictFromCache(dictId); + if (dict == null) { + errorMessage = "数据验证失败,字典Id并不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(dict.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用并不包含该字典Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!dict.getDictType().equals(DictType.TABLE) + && !dict.getDictType().equals(DictType.GLOBAL_DICT)) { + errorMessage = "数据验证失败,该接口仅支持数据表字典和全局编码字典!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (dict.getDictType().equals(DictType.GLOBAL_DICT)) { + List dictItems = + globalDictService.getGlobalDictItemListFromCache(dict.getDictCode(), null); + List> dataMapList = + MyCommonUtil.toDictDataList(dictItems, GlobalDictItem::getItemId, GlobalDictItem::getItemName); + return ResponseResult.success(dataMapList); + } + if (CollUtil.isNotEmpty(filterDtoList)) { + for (OnlineFilterDto filter : filterDtoList) { + if (!this.checkTableAndColumnName(filter.getColumnName())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤字段名 [{}] 包含 (数字、字母和下划线) 之外的非法字符!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + } + List> resultList = onlineOperationService.getDictDataList(dict, filterDtoList); + return ResponseResult.success(resultList); + } + + /** + * 获取在线表单所关联的权限数据,包括权限字列表和权限资源列表。 + * 注:该接口仅用于微服务间调用使用,无需对前端开放。 + * + * @param menuFormIds 菜单关联的表单Id集合。 + * @param viewFormIds 查询权限的表单Id集合。 + * @param editFormIds 编辑权限的表单Id集合。 + * @return 参数中在线表单所关联的权限数据。 + */ + @GetMapping("/calculatePermData") + public ResponseResult> calculatePermData( + @RequestParam Set menuFormIds, + @RequestParam Set viewFormIds, + @RequestParam Set editFormIds) { + return ResponseResult.success(onlineOperationService.calculatePermData(menuFormIds, viewFormIds, editFormIds)); + } + + private ResponseResult doDelete( + String datasourceVariableName, Long datasourceId, List dataIdList) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + OnlineTable masterTable = datasource.getMasterTable(); + ResponseResult> relationListResult = + onlineOperationHelper.verifyAndGetRelationList(datasourceId, RelationType.ONE_TO_MANY); + if (!relationListResult.isSuccess()) { + return ResponseResult.errorFrom(relationListResult); + } + List relationList = relationListResult.getData(); + for (String dataId : dataIdList) { + if (!onlineOperationService.delete(masterTable, relationList, dataId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } + return ResponseResult.success(); + } + + private ResponseResult doDelete( + String datasourceVariableName, Long datasourceId, Long relationId, List dataIdList) { + ResponseResult verifyResult = + this.doVerifyAndGetRelation(datasourceId, datasourceVariableName, relationId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineDatasourceRelation relation = verifyResult.getData(); + for (String dataId : dataIdList) { + if (!onlineOperationService.delete(relation.getSlaveTable(), null, dataId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyAndGetDatasource( + Long datasourceId, String datasourceVariableName) { + ResponseResult datasourceResult = + onlineOperationHelper.verifyAndGetDatasource(datasourceId); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + return ResponseResult.success(datasource); + } + + private ResponseResult doVerifyAndGetRelation( + Long datasourceId, String datasourceVariableName, Long relationId) { + OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceFromCache(datasourceId); + if (datasource == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,数据源Id并不存在!"); + } + if (!datasource.getVariableName().equals(datasourceVariableName)) { + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + return onlineOperationHelper.verifyAndGetRelation(datasourceId, relationId); + } + + private ResponseResult verifyFilterDtoList(List filterDtoList) { + if (CollUtil.isEmpty(filterDtoList)) { + return ResponseResult.success(); + } + String errorMessage; + for (OnlineFilterDto filter : filterDtoList) { + if (!this.checkTableAndColumnName(filter.getTableName())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤表名 [{}] 包含 (数字、字母和下划线) 之外的非法字符!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!this.checkTableAndColumnName(filter.getColumnName())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤字段名 [{}] 包含 (数字、字母和下划线) 之外的非法字符!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!filter.getFilterType().equals(FieldFilterType.RANGE_FILTER) + && ObjectUtil.isEmpty(filter.getColumnValue())) { + errorMessage = StrFormatter.format( + "数据验证失败,过滤字段名 [{}] 过滤值不能为空!", filter.getColumnName()); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + return ResponseResult.success(); + } + + private boolean checkTableAndColumnName(String name) { + if (StrUtil.isBlank(name)) { + return true; + } + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (!CharUtil.isLetterOrNumber(c) && !CharUtil.equals('_', c, false)) { + return false; + } + } + return true; + } + + private ResponseResult makeOrderBy( + MyOrderParam orderParam, OnlineTable masterTable, Map tableMap) { + if (CollUtil.isEmpty(orderParam)) { + return ResponseResult.success(null); + } + String errorMessage; + StringBuilder sb = new StringBuilder(128); + for (MyOrderParam.OrderInfo orderInfo : orderParam) { + String[] orderArray = StrUtil.splitToArray(orderInfo.getFieldName(), '.'); + // 如果没有前缀,我们就可以默认为主表的字段。 + if (orderArray.length == 1) { + try { + sb.append(this.makeOrderByForOrderInfo(masterTable, orderArray[0], orderInfo)); + } catch (OnlineRuntimeException e) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, e.getMessage()); + } + } else { + String tableName = orderArray[0]; + String columnName = orderArray[1]; + OnlineTable table = tableMap.get(tableName); + if (table == null) { + errorMessage = StrFormatter.format( + "数据验证失败,排序字段 [{}] 的数据表 [{}] 并不属于当前数据源!", + orderInfo.getFieldName(), tableName); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + try { + sb.append(this.makeOrderByForOrderInfo(table, columnName, orderInfo)); + } catch (OnlineRuntimeException e) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, e.getMessage()); + } + } + } + return ResponseResult.success(sb.substring(0, sb.length() - 2)); + } + + private String makeOrderByForOrderInfo( + OnlineTable table, String columnName, MyOrderParam.OrderInfo orderInfo) { + StringBuilder sb = new StringBuilder(64); + boolean found = false; + for (OnlineColumn column : table.getColumnMap().values()) { + if (column.getColumnName().equals(columnName)) { + sb.append(table.getTableName()).append(".").append(columnName); + if (BooleanUtil.isFalse(orderInfo.getAsc())) { + sb.append(" DESC"); + } + sb.append(", "); + found = true; + break; + } + } + if (!found) { + String errorMessage = StrFormatter.format( + "数据验证失败,排序字段 [{}] 在数据表 [{}] 中并不存在!", + orderInfo.getFieldName(), table.getTableName()); + throw new OnlineRuntimeException(errorMessage); + } + return sb.toString(); + } + + private int makeImportHeaderInfoByFieldType(String objectFieldType) { + return switch (objectFieldType) { + case ObjectFieldType.INTEGER -> ImportUtil.INT_TYPE; + case ObjectFieldType.LONG -> ImportUtil.LONG_TYPE; + case ObjectFieldType.STRING -> ImportUtil.STRING_TYPE; + case ObjectFieldType.BOOLEAN -> ImportUtil.BOOLEAN_TYPE; + case ObjectFieldType.DATE -> ImportUtil.DATE_TYPE; + case ObjectFieldType.DOUBLE -> ImportUtil.DOUBLE_TYPE; + case ObjectFieldType.BIG_DECIMAL -> ImportUtil.BIG_DECIMAL_TYPE; + default -> throw new MyRuntimeException("Unsupport Import FieldType"); + }; + } + + private Map makeExportHeaderMap( + OnlineTable masterTable, + List allRelationList, + List exportInfoList) { + Map headerMap = new LinkedHashMap<>(16); + Map allRelationMap = null; + if (allRelationList != null) { + allRelationMap = allRelationList.stream() + .collect(Collectors.toMap(OnlineDatasourceRelation::getSlaveTableId, r -> r)); + } + for (ExportInfo exportInfo : exportInfoList) { + if (exportInfo.getVirtualColumnId() != null) { + OnlineVirtualColumn virtualColumn = + onlineVirtualColumnService.getById(exportInfo.getVirtualColumnId()); + if (virtualColumn != null) { + headerMap.put(virtualColumn.getObjectFieldName(), exportInfo.showName); + } + continue; + } + if (masterTable != null && exportInfo.getTableId().equals(masterTable.getTableId())) { + OnlineColumn column = masterTable.getColumnMap().get(exportInfo.getColumnId()); + String columnName = this.appendSuffixForDictColumn(column, column.getColumnName()); + headerMap.put(columnName, exportInfo.getShowName()); + } else { + OnlineDatasourceRelation relation = + MapUtil.get(allRelationMap, exportInfo.getTableId(), OnlineDatasourceRelation.class); + if (relation != null) { + OnlineColumn column = relation.getSlaveTable().getColumnMap().get(exportInfo.getColumnId()); + String columnName = this.appendSuffixForDictColumn( + column, relation.getVariableName() + "." + column.getColumnName()); + headerMap.put(columnName, exportInfo.getShowName()); + } + } + } + return headerMap; + } + + private void normalizeExportDataList(List> dataList) { + for (Map columnData : dataList) { + for (Map.Entry entry : columnData.entrySet()) { + if (entry.getValue() instanceof Long || entry.getValue() instanceof BigDecimal) { + columnData.put(entry.getKey(), entry.getValue() == null ? "" : entry.getValue().toString()); + } + } + } + } + + private String appendSuffixForDictColumn(OnlineColumn column, String columnName) { + if (column.getDictId() != null) { + if (ObjectUtil.equal(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + columnName += "DictMapList"; + } else { + columnName += "DictMap.name"; + } + } + return columnName; + } + + @Data + public static class ExportInfo { + private Long tableId; + private Long columnId; + private Long virtualColumnId; + private String showName; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java new file mode 100644 index 00000000..25bbedb9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlinePageController.java @@ -0,0 +1,386 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineDatasourceDto; +import com.orangeforms.common.online.dto.OnlinePageDatasourceDto; +import com.orangeforms.common.online.dto.OnlinePageDto; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineForm; +import com.orangeforms.common.online.model.OnlinePage; +import com.orangeforms.common.online.model.OnlinePageDatasource; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineFormService; +import com.orangeforms.common.online.service.OnlinePageService; +import com.orangeforms.common.online.vo.OnlineDatasourceVo; +import com.orangeforms.common.online.vo.OnlinePageDatasourceVo; +import com.orangeforms.common.online.vo.OnlinePageVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 在线表单页面接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单页面接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlinePage") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlinePageController { + + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + + /** + * 新增在线表单页面数据。 + * + * @param onlinePageDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlinePageDto.pageId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlinePageDto onlinePageDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlinePage onlinePage = MyModelUtil.copyTo(onlinePageDto, OnlinePage.class); + if (onlinePageService.existByPageCode(onlinePage.getPageCode())) { + errorMessage = "数据验证失败,页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + try { + onlinePage = onlinePageService.saveNew(onlinePage); + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(onlinePage.getPageId()); + } + + /** + * 更新在线表单页面数据。 + * + * @param onlinePageDto 更新对象。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlinePageDto onlinePageDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlinePage onlinePage = MyModelUtil.copyTo(onlinePageDto, OnlinePage.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlinePage.getPageId()); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePage originalOnlinePage = verifyResult.getData(); + if (!onlinePage.getPageType().equals(originalOnlinePage.getPageType())) { + errorMessage = "数据验证失败,页面类型不能修改!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!StrUtil.equals(onlinePage.getPageCode(), originalOnlinePage.getPageCode()) + && onlinePageService.existByPageCode(onlinePage.getPageCode())) { + errorMessage = "数据验证失败,页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); + } + try { + if (!onlinePageService.update(onlinePage, originalOnlinePage)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + } catch (DuplicateKeyException e) { + errorMessage = "数据验证失败,当前应用的页面编码已经存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 更新在线表单页面对象的发布状态字段。 + * + * @param pageId 待更新的页面对象主键Id。 + * @param published 发布状态。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/updatePublished") + public ResponseResult updateStatus( + @MyRequestBody(required = true) Long pageId, + @MyRequestBody(required = true) Boolean published) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePage originalOnlinePage = verifyResult.getData(); + if (!published.equals(originalOnlinePage.getPublished())) { + if (BooleanUtil.isTrue(published) && !originalOnlinePage.getStatus().equals(PageStatus.FORM_DESIGN)) { + errorMessage = "数据验证失败,当前页面状态不为 [设计] 状态,因此不能发布!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + onlinePageService.updatePublished(pageId, published); + } + return ResponseResult.success(); + } + + /** + * 删除在线表单页面数据。 + * + * @param pageId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long pageId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlinePageService.remove(pageId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的在线表单页面列表。 + * + * @param onlinePageDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlinePageDto onlinePageDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlinePage onlinePageFilter = MyModelUtil.copyTo(onlinePageDtoFilter, OnlinePage.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlinePage.class); + List onlinePageList = onlinePageService.getOnlinePageListWithRelation(onlinePageFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlinePageList, OnlinePageVo.class)); + } + + /** + * 获取系统中配置的所有Page和表单的列表。 + * + * @return 系统中配置的所有Page和表单的列表。 + */ + @PostMapping("/listAllPageAndForm") + public ResponseResult listAllPageAndForm() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("pageList", onlinePageService.getOnlinePageList(null, null)); + List formList = onlineFormService.getOnlineFormList(null, null); + formList.forEach(f -> f.setWidgetJson(null)); + jsonObject.put("formList", formList); + return ResponseResult.success(jsonObject); + } + + /** + * 查看指定在线表单页面对象详情。 + * + * @param pageId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long pageId) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePage onlinePage = onlinePageService.getByIdWithRelation(pageId, MyRelationParam.full()); + if (onlinePage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlinePage, OnlinePageVo.class); + } + + /** + * 列出与指定在线表单页面存在多对多关系的在线数据源列表数据。 + * + * @param pageId 主表关联字段。 + * @param onlineDatasourceDtoFilter 在线数据源过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,返回符合条件的数据列表。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/listOnlinePageDatasource") + public ResponseResult> listOnlinePageDatasource( + @MyRequestBody Long pageId, + @MyRequestBody OnlineDatasourceDto onlineDatasourceDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineDatasource filter = MyModelUtil.copyTo(onlineDatasourceDtoFilter, OnlineDatasource.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineDatasource.class); + List onlineDatasourceList = + onlineDatasourceService.getOnlineDatasourceListByPageId(pageId, filter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineDatasourceList, OnlineDatasourceVo.class)); + } + + /** + * 批量添加在线表单页面和在线数据源对象的多对多关联关系数据。 + * + * @param pageId 主表主键Id。 + * @param onlinePageDatasourceDtoList 关联对象列表。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD_M2M) + @PostMapping("/addOnlinePageDatasource") + public ResponseResult addOnlinePageDatasource( + @MyRequestBody Long pageId, + @MyRequestBody(value = "onlinePageDatasourceList") List onlinePageDatasourceDtoList) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (MyCommonUtil.existBlankArgument(onlinePageDatasourceDtoList)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + for (OnlinePageDatasourceDto onlinePageDatasource : onlinePageDatasourceDtoList) { + errorMessage = MyCommonUtil.getModelValidationError(onlinePageDatasource); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + Set datasourceIdSet = onlinePageDatasourceDtoList.stream() + .map(OnlinePageDatasourceDto::getDatasourceId).collect(Collectors.toSet()); + List datasourceList = onlineDatasourceService.getInList(datasourceIdSet); + if (datasourceIdSet.size() != datasourceList.size()) { + errorMessage = "数据验证失败,当前在线表单包含不存在的数据源Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + String appCode = TokenData.takeFromRequest().getAppCode(); + for (OnlineDatasource datasource : datasourceList) { + if (!StrUtil.equals(datasource.getAppCode(), appCode)) { + errorMessage = "数据验证失败,存在不是当前应用的数据源!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + List onlinePageDatasourceList = + MyModelUtil.copyCollectionTo(onlinePageDatasourceDtoList, OnlinePageDatasource.class); + onlinePageService.addOnlinePageDatasourceList(onlinePageDatasourceList, pageId); + return ResponseResult.success(); + } + + /** + * 显示在线表单页面和指定数据源的多对多关联详情数据。 + * + * @param pageId 主表主键Id。 + * @param datasourceId 从表主键Id。 + * @return 应答结果对象,包括中间表详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/viewOnlinePageDatasource") + public ResponseResult viewOnlinePageDatasource( + @RequestParam Long pageId, @RequestParam Long datasourceId) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlinePageDatasource onlinePageDatasource = onlinePageService.getOnlinePageDatasource(pageId, datasourceId); + if (onlinePageDatasource == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + OnlinePageDatasourceVo onlinePageDatasourceVo = + MyModelUtil.copyTo(onlinePageDatasource, OnlinePageDatasourceVo.class); + return ResponseResult.success(onlinePageDatasourceVo); + } + + /** + * 移除指定在线表单页面和指定数据源的多对多关联关系。 + * + * @param pageId 主表主键Id。 + * @param datasourceId 从表主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE_M2M) + @PostMapping("/deleteOnlinePageDatasource") + public ResponseResult deleteOnlinePageDatasource( + @MyRequestBody Long pageId, @MyRequestBody(required = true) Long datasourceId) { + ResponseResult verifyResult = this.doVerifyAndGet(pageId); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlinePageService.removeOnlinePageDatasource(pageId, datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + private ResponseResult doVerifyAndGet(Long pageId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(pageId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlinePage onlinePage = onlinePageService.getById(pageId); + if (onlinePage == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + TokenData tokenData = TokenData.takeFromRequest(); + if (!StrUtil.equals(onlinePage.getAppCode(), tokenData.getAppCode())) { + errorMessage = "数据验证失败,当前应用不存在该页面!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (ObjectUtil.notEqual(onlinePage.getTenantId(), tokenData.getTenantId())) { + errorMessage = "数据验证失败,当前租户不包含该页面!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(onlinePage); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java new file mode 100644 index 00000000..b5491b5a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineRuleController.java @@ -0,0 +1,175 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.BooleanUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.MyPageUtil; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineRuleDto; +import com.orangeforms.common.online.model.OnlineRule; +import com.orangeforms.common.online.service.OnlineRuleService; +import com.orangeforms.common.online.vo.OnlineRuleVo; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.groups.Default; +import java.util.List; + +/** + * 在线表单字段验证规则接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单字段验证规则接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineRule") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineRuleController { + + @Autowired + private OnlineRuleService onlineRuleService; + + /** + * 新增验证规则数据。 + * + * @param onlineRuleDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineRuleDto.ruleId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineRuleDto onlineRuleDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineRuleDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineRule onlineRule = MyModelUtil.copyTo(onlineRuleDto, OnlineRule.class); + onlineRule = onlineRuleService.saveNew(onlineRule); + return ResponseResult.success(onlineRule.getRuleId()); + } + + /** + * 更新验证规则数据。 + * + * @param onlineRuleDto 更新对象。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.UPDATE) + @SaCheckPermission("onlinePage.all") + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineRuleDto onlineRuleDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineRuleDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineRule onlineRule = MyModelUtil.copyTo(onlineRuleDto, OnlineRule.class); + ResponseResult verifyResult = this.doVerifyAndGet(onlineRule.getRuleId(), false); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineRule originalOnlineRule = verifyResult.getData(); + if (!onlineRuleService.update(onlineRule, originalOnlineRule)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除验证规则数据。 + * + * @param ruleId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long ruleId) { + String errorMessage; + ResponseResult verifyResult = this.doVerifyAndGet(ruleId, false); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineRuleService.remove(ruleId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的验证规则列表。 + * + * @param onlineRuleDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineRuleDto onlineRuleDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineRule onlineRuleFilter = MyModelUtil.copyTo(onlineRuleDtoFilter, OnlineRule.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineRule.class); + List onlineRuleList = onlineRuleService.getOnlineRuleListWithRelation(onlineRuleFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(onlineRuleList, OnlineRuleVo.class)); + } + + /** + * 查看指定验证规则对象详情。 + * + * @param ruleId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long ruleId) { + ResponseResult verifyResult = this.doVerifyAndGet(ruleId, true); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + OnlineRule onlineRule = verifyResult.getData(); + return ResponseResult.success(onlineRule, OnlineRuleVo.class); + } + + private ResponseResult doVerifyAndGet(Long ruleId, boolean readOnly) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(ruleId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineRule rule = onlineRuleService.getById(ruleId); + if (rule == null) { + errorMessage = "数据验证失败,当前在线字段规则并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!readOnly && BooleanUtil.isTrue(rule.getBuiltin())) { + errorMessage = "数据验证失败,内置规则不能删除!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (!StrUtil.equals(rule.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用并不包含该规则!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + return ResponseResult.success(rule); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java new file mode 100644 index 00000000..f28e81d1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/controller/OnlineVirtualColumnController.java @@ -0,0 +1,195 @@ +package com.orangeforms.common.online.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.tags.Tag; +import com.github.pagehelper.page.PageMethod; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.annotation.MyRequestBody; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.log.annotation.OperationLog; +import com.orangeforms.common.log.model.constant.SysOperationLogType; +import com.orangeforms.common.online.dto.OnlineVirtualColumnDto; +import com.orangeforms.common.online.model.OnlineVirtualColumn; +import com.orangeforms.common.online.model.constant.VirtualType; +import com.orangeforms.common.online.service.OnlineVirtualColumnService; +import com.orangeforms.common.online.vo.OnlineVirtualColumnVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import jakarta.validation.groups.Default; + +/** + * 在线表单虚拟字段接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Tag(name = "在线表单虚拟字段接口") +@Slf4j +@RestController +@RequestMapping("${common-online.urlPrefix}/onlineVirtualColumn") +@ConditionalOnProperty(name = "common-online.operationEnabled", havingValue = "true") +public class OnlineVirtualColumnController { + + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + + /** + * 新增虚拟字段数据。 + * + * @param onlineVirtualColumnDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = {"onlineVirtualColumnDto.virtualColumnId"}) + @SaCheckPermission("onlinePage.all") + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody OnlineVirtualColumnDto onlineVirtualColumnDto) { + String errorMessage = MyCommonUtil.getModelValidationError(onlineVirtualColumnDto); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineVirtualColumn onlineVirtualColumn = + MyModelUtil.copyTo(onlineVirtualColumnDto, OnlineVirtualColumn.class); + ResponseResult verifyResult = this.doVerify(onlineVirtualColumn, null); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + onlineVirtualColumn = onlineVirtualColumnService.saveNew(onlineVirtualColumn); + return ResponseResult.success(onlineVirtualColumn.getVirtualColumnId()); + } + + /** + * 更新虚拟字段数据。 + * + * @param onlineVirtualColumnDto 更新对象。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.UPDATE) + @SaCheckPermission("onlinePage.all") + @PostMapping("/update") + public ResponseResult update(@MyRequestBody OnlineVirtualColumnDto onlineVirtualColumnDto) { + String errorMessage = MyCommonUtil.getModelValidationError( + onlineVirtualColumnDto, Default.class, UpdateGroup.class); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineVirtualColumn onlineVirtualColumn = + MyModelUtil.copyTo(onlineVirtualColumnDto, OnlineVirtualColumn.class); + OnlineVirtualColumn originalOnlineVirtualColumn = + onlineVirtualColumnService.getById(onlineVirtualColumn.getVirtualColumnId()); + if (originalOnlineVirtualColumn == null) { + errorMessage = "数据验证失败,当前虚拟字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + ResponseResult verifyResult = this.doVerify(onlineVirtualColumn, originalOnlineVirtualColumn); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + if (!onlineVirtualColumnService.update(onlineVirtualColumn, originalOnlineVirtualColumn)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除虚拟字段数据。 + * + * @param virtualColumnId 删除对象主键Id。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.DELETE) + @SaCheckPermission("onlinePage.all") + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long virtualColumnId) { + String errorMessage; + if (MyCommonUtil.existBlankArgument(virtualColumnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + // 验证关联Id的数据合法性 + OnlineVirtualColumn originalOnlineVirtualColumn = onlineVirtualColumnService.getById(virtualColumnId); + if (originalOnlineVirtualColumn == null) { + errorMessage = "数据验证失败,当前虚拟字段并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!onlineVirtualColumnService.remove(virtualColumnId)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 列出符合过滤条件的虚拟字段列表。 + * + * @param onlineVirtualColumnDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @SaCheckPermission("onlinePage.all") + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody OnlineVirtualColumnDto onlineVirtualColumnDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + OnlineVirtualColumn onlineVirtualColumnFilter = + MyModelUtil.copyTo(onlineVirtualColumnDtoFilter, OnlineVirtualColumn.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, OnlineVirtualColumn.class); + List onlineVirtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnListWithRelation(onlineVirtualColumnFilter, orderBy); + MyPageData pageData = + MyPageUtil.makeResponseData(onlineVirtualColumnList, OnlineVirtualColumnVo.class); + return ResponseResult.success(pageData); + } + + /** + * 查看指定虚拟字段对象详情。 + * + * @param virtualColumnId 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @SaCheckPermission("onlinePage.all") + @GetMapping("/view") + public ResponseResult view(@RequestParam Long virtualColumnId) { + if (MyCommonUtil.existBlankArgument(virtualColumnId)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + OnlineVirtualColumn onlineVirtualColumn = + onlineVirtualColumnService.getByIdWithRelation(virtualColumnId, MyRelationParam.full()); + if (onlineVirtualColumn == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(onlineVirtualColumn, OnlineVirtualColumnVo.class); + } + + private ResponseResult doVerify( + OnlineVirtualColumn virtualColumn, OnlineVirtualColumn originalVirtualColumn) { + if (!virtualColumn.getVirtualType().equals(VirtualType.AGGREGATION)) { + return ResponseResult.success(); + } + if (MyCommonUtil.existBlankArgument( + virtualColumn.getAggregationColumnId(), + virtualColumn.getAggregationTableId(), + virtualColumn.getDatasourceId(), + virtualColumn.getRelationId(), + virtualColumn.getAggregationType())) { + String errorMessage = "数据验证失败,数据源、关联关系、聚合表、聚合字段和聚合类型,均不能为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CallResult verifyResult = onlineVirtualColumnService.verifyRelatedData(virtualColumn, originalVirtualColumn); + if (!verifyResult.isSuccess()) { + return ResponseResult.errorFrom(verifyResult); + } + return ResponseResult.success(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java new file mode 100644 index 00000000..fbfc638f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnMapper.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 字段数据数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineColumnMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineColumnFilter 主表过滤对象。 + * @return 对象列表。 + */ + List getOnlineColumnList(@Param("onlineColumnFilter") OnlineColumn onlineColumnFilter); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java new file mode 100644 index 00000000..84128efd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineColumnRuleMapper.java @@ -0,0 +1,14 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineColumnRule; + +/** + * 数据字段规则访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineColumnRuleMapper extends BaseDaoMapper { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java new file mode 100644 index 00000000..7f5aaca2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceMapper.java @@ -0,0 +1,60 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDatasource; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 数据模型数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineDatasourceFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDatasourceList( + @Param("onlineDatasourceFilter") OnlineDatasource onlineDatasourceFilter, @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表数据列表。 + * + * @param pageId 关联主表Id。 + * @param onlineDatasourceFilter 从表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 从表数据列表。 + */ + List getOnlineDatasourceListByPageId( + @Param("pageId") Long pageId, + @Param("onlineDatasourceFilter") OnlineDatasource onlineDatasourceFilter, + @Param("orderBy") String orderBy); + + /** + * 根据在线表单Id集合,获取关联的在线数据源对象列表。 + * + * @param formIdSet 在线表单Id集合。 + * @return 与参数表单Id关联的数据源列表。 + */ + List getOnlineDatasourceListByFormIds(@Param("formIdSet") Set formIdSet); + + /** + * 获取在线表单页面和在线表单数据源变量名的映射关系。 + * + * @param pageIds 页面Id集合。 + * @return 在线表单页面和在线表单数据源变量名的映射关系。 + */ + @Select("SELECT a.page_id, b.variable_name FROM zz_online_page_datasource a, zz_online_datasource b" + + " WHERE a.page_id in (${pageIds}) AND a.datasource_id = b.datasource_id") + List> getPageIdAndVariableNameMapByPageIds(@Param("pageIds") String pageIds); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java new file mode 100644 index 00000000..d68c13a2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceRelationMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据关联数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceRelationMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDatasourceRelationList( + @Param("filter") OnlineDatasourceRelation filter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java new file mode 100644 index 00000000..a84fbb66 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDatasourceTableMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDatasourceTable; + +/** + * 数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceTableMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java new file mode 100644 index 00000000..1941c7f8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDblinkMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDblink; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据库链接数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDblinkMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineDblinkFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDblinkList( + @Param("onlineDblinkFilter") OnlineDblink onlineDblinkFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java new file mode 100644 index 00000000..b22cca72 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineDictMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineDict; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在线表单字典数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDictMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineDictFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineDictList( + @Param("onlineDictFilter") OnlineDict onlineDictFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java new file mode 100644 index 00000000..a8485da4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormDatasourceMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineFormDatasource; + +/** + * 在线表单与数据源多对多关联的数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineFormDatasourceMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java new file mode 100644 index 00000000..5adbad02 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineFormMapper.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineForm; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在线表单数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineFormMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineFormFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineFormList( + @Param("onlineFormFilter") OnlineForm onlineFormFilter, @Param("orderBy") String orderBy); + + /** + * 根据数据源Id,返回使用该数据源的OnlineForm对象。 + * + * @param datasourceId 数据源Id。 + * @param onlineFormFilter 主表过滤对象。 + * @return 使用该数据源的表单列表。 + */ + List getOnlineFormListByDatasourceId( + @Param("datasourceId") Long datasourceId, @Param("onlineFormFilter") OnlineForm onlineFormFilter); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java new file mode 100644 index 00000000..025e437c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineOperationMapper.java @@ -0,0 +1,259 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.object.ColumnData; +import com.orangeforms.common.online.object.JoinTableInfo; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; + +/** + * 在线表单运行时数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Mapper +public interface OnlineOperationMapper { + + /** + * 插入新数据。 + * + * @param tableName 数据表名。 + * @param columnNames 字段名列表。 + * @param columnValueList 字段值列表。 + */ + @Insert("") + void insert( + @Param("tableName") String tableName, + @Param("columnNames") String columnNames, + @Param("columnValueList") List columnValueList); + + /** + * 更新表数据。 + * + * @param tableName 数据表名。 + * @param updateColumnList 更新字段列表。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @return 更新行数。 + */ + @Update("") + int update( + @Param("tableName") String tableName, + @Param("updateColumnList") List updateColumnList, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter); + + /** + * 删除指定数据。 + * + * @param tableName 表名。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @return 删除行数。 + */ + @Delete("") + int delete( + @Param("tableName") String tableName, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter); + + /** + * 执行动态查询,并返回查询结果集。 + * + * @param masterTableName 主表名称。 + * @param joinInfoList 关联表信息列表。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @param orderBy 排序字符串。 + * @return 查询结果集。 + */ + @Select("") + List> getList( + @Param("masterTableName") String masterTableName, + @Param("joinInfoList") List joinInfoList, + @Param("selectFields") String selectFields, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter, + @Param("orderBy") String orderBy); + + /** + * 以字典键值对的方式返回数据。 + * + * @param tableName 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param filterList SQL过滤条件列表。 + * @param dataPermFilter 数据权限过滤字符串。 + * @return 查询结果集。 + */ + @Select("") + List> getDictList( + @Param("tableName") String tableName, + @Param("selectFields") String selectFields, + @Param("filterList") List filterList, + @Param("dataPermFilter") String dataPermFilter); + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy 分组字段列表,逗号分隔。 + * @return 对象可选字段Map列表。 + */ + @Select("") + List> getGroupedListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("groupBy") String groupBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java new file mode 100644 index 00000000..d486645d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageDatasourceMapper.java @@ -0,0 +1,13 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlinePageDatasource; + +/** + * 在线表单页面和数据源关联对象的数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlinePageDatasourceMapper extends BaseDaoMapper { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java new file mode 100644 index 00000000..7ac0841f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlinePageMapper.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlinePage; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在线表单页面数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlinePageMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlinePageFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlinePageList( + @Param("onlinePageFilter") OnlinePage onlinePageFilter, @Param("orderBy") String orderBy); + + /** + /** + * 根据数据源Id,返回使用该数据源的OnlinePage对象。 + * + * @param datasourceId 数据源Id。 + * @return 使用该数据源的页面列表。 + */ + List getOnlinePageListByDatasourceId( + @Param("datasourceId") Long datasourceId, @Param("onlinePageFilter") OnlinePage onlinePageFilter); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java new file mode 100644 index 00000000..245ba10b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineRuleMapper.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineRule; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 验证规则数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineRuleMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineRuleFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineRuleList( + @Param("onlineRuleFilter") OnlineRule onlineRuleFilter, @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表数据列表。 + * + * @param columnId 关联主表Id。 + * @param onlineRuleFilter 从表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 从表数据列表。 + */ + List getOnlineRuleListByColumnId( + @Param("columnId") Long columnId, + @Param("onlineRuleFilter") OnlineRule onlineRuleFilter, + @Param("orderBy") String orderBy); + + /** + * 根据关联主表Id,获取关联从表中没有和主表建立关联关系的数据列表。 + * + * @param columnId 关联主表Id。 + * @param onlineRuleFilter 过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 与主表没有建立关联的从表数据列表。 + */ + List getNotInOnlineRuleListByColumnId( + @Param("columnId") Long columnId, + @Param("onlineRuleFilter") OnlineRule onlineRuleFilter, + @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java new file mode 100644 index 00000000..238c0bae --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineTableMapper.java @@ -0,0 +1,34 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineTable; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 数据表数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineTableMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineTableFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineTableList( + @Param("onlineTableFilter") OnlineTable onlineTableFilter, @Param("orderBy") String orderBy); + + /** + * 根据数据源Id,获取该数据源及其关联所引用的数据表列表。 + * + * @param datasourceId 指定的数据源Id。 + * @return 该数据源及其关联所引用的数据表列表。 + */ + List getOnlineTableListByDatasourceId(@Param("datasourceId") Long datasourceId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java new file mode 100644 index 00000000..78ca3d20 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/OnlineVirtualColumnMapper.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.online.dao; + +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.online.model.OnlineVirtualColumn; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 虚拟字段数据操作访问接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineVirtualColumnMapper extends BaseDaoMapper { + + /** + * 获取过滤后的对象列表。 + * + * @param onlineVirtualColumnFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getOnlineVirtualColumnList( + @Param("onlineVirtualColumnFilter") OnlineVirtualColumn onlineVirtualColumnFilter, @Param("orderBy") String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml new file mode 100644 index 00000000..ede95b2e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnMapper.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_column.table_id = #{onlineColumnFilter.tableId} + + + AND zz_online_column.column_name = #{onlineColumnFilter.columnName} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml new file mode 100644 index 00000000..c5afda31 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineColumnRuleMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml new file mode 100644 index 00000000..b148a15b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_datasource.app_code IS NULL + + + AND zz_online_datasource.app_code = #{onlineDatasourceFilter.appCode} + + + AND zz_online_datasource.variable_name = #{onlineDatasourceFilter.variableName} + + + AND zz_online_datasource.datasource_name = #{onlineDatasourceFilter.datasourceName} + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml new file mode 100644 index 00000000..c669d3d2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceRelationMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_datasource_relation.app_code IS NULL + + + AND zz_online_datasource_relation.app_code = #{filter.appCode} + + + AND zz_online_datasource_relation.relation_name = #{filter.relationName} + + + AND zz_online_datasource_relation.datasource_id = #{filter.datasourceId} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml new file mode 100644 index 00000000..d3ba6aaa --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDatasourceTableMapper.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml new file mode 100644 index 00000000..59f94b1e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDblinkMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_dblink.app_code IS NULL + + + AND zz_online_dblink.app_code = #{onlineDblinkFilter.appCode} + + + AND zz_online_dblink.dblink_type = #{onlineDblinkFilter.dblinkType} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml new file mode 100644 index 00000000..cf1fa27e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineDictMapper.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_dict.dict_id = #{onlineDictFilter.dictId} + + + AND zz_online_dict.app_code IS NULL + + + AND zz_online_dict.app_code = #{onlineDictFilter.appCode} + + + AND zz_online_dict.dict_name = #{onlineDictFilter.dictName} + + + AND zz_online_dict.dict_type = #{onlineDictFilter.dictType} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml new file mode 100644 index 00000000..5d0924ff --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormDatasourceMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml new file mode 100644 index 00000000..a79415be --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineFormMapper.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_form.tenant_id IS NULL + + + AND zz_online_form.tenant_id = #{onlineFormFilter.tenantId} + + + AND zz_online_form.app_code IS NULL + + + AND zz_online_form.app_code = #{onlineFormFilter.appCode} + + + AND zz_online_form.page_id = #{onlineFormFilter.pageId} + + + AND zz_online_form.form_code = #{onlineFormFilter.formCode} + + + + AND zz_online_form.form_name LIKE #{safeFormName} + + + AND zz_online_form.form_type = #{onlineFormFilter.formType} + + + AND zz_online_form.master_table_id = #{onlineFormFilter.masterTableId} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml new file mode 100644 index 00000000..47d8b88d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageDatasourceMapper.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml new file mode 100644 index 00000000..86aeeb21 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlinePageMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_page.tenant_id IS NULL + + + AND zz_online_page.tenant_id = #{onlinePageFilter.tenantId} + + + AND zz_online_page.app_code IS NULL + + + AND zz_online_page.app_code = #{onlinePageFilter.appCode} + + + AND zz_online_page.page_code = #{onlinePageFilter.pageCode} + + + + AND zz_online_page.page_name LIKE #{safePageName} + + + AND zz_online_page.page_type = #{onlinePageFilter.pageType} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml new file mode 100644 index 00000000..35095622 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineRuleMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + AND (zz_online_rule.app_code IS NULL OR zz_online_rule.builtin = 1) + + + AND (zz_online_rule.app_code = #{onlineRuleFilter.appCode} OR zz_online_rule.builtin = 1) + + + AND zz_online_rule.deleted_flag = ${@com.orangeforms.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml new file mode 100644 index 00000000..abb2569b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineTableMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_table.app_code IS NULL + + + AND zz_online_table.app_code = #{onlineTableFilter.appCode} + + + AND zz_online_table.table_name = #{onlineTableFilter.tableName} + + + AND zz_online_table.model_name = #{onlineTableFilter.modelName} + + + AND zz_online_table.dblink_id = #{onlineTableFilter.dblinkId} + + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml new file mode 100644 index 00000000..1dbc69e8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dao/mapper/OnlineVirtualColumnMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND zz_online_virtual_column.datasource_id = #{onlineVirtualColumnFilter.datasourceId} + + + AND zz_online_virtual_column.relation_id = #{onlineVirtualColumnFilter.relationId} + + + AND zz_online_virtual_column.table_id = #{onlineVirtualColumnFilter.tableId} + + + AND zz_online_virtual_column.aggregation_column_id = #{onlineVirtualColumnFilter.aggregationColumnId} + + + AND zz_online_virtual_column.virtual_type = #{onlineVirtualColumnFilter.virtualType} + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java new file mode 100644 index 00000000..a3713cbf --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnDto.java @@ -0,0 +1,189 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.model.constant.FieldKind; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表字段Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段Dto对象") +@Data +public class OnlineColumnDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long columnId; + + /** + * 字段名。 + */ + @Schema(description = "字段名") + @NotBlank(message = "数据验证失败,字段名不能为空!") + private String columnName; + + /** + * 数据表Id。 + */ + @Schema(description = "数据表Id") + @NotNull(message = "数据验证失败,数据表Id不能为空!") + private Long tableId; + + /** + * 数据表中的字段类型。 + */ + @Schema(description = "数据表中的字段类型") + @NotBlank(message = "数据验证失败,数据表中的字段类型不能为空!") + private String columnType; + + /** + * 数据表中的完整字段类型(包括了精度和刻度)。 + */ + @Schema(description = "数据表中的完整字段类型") + @NotBlank(message = "数据验证失败,数据表中的完整字段类型(包括了精度和刻度)不能为空!") + private String fullColumnType; + + /** + * 是否为主键。 + */ + @Schema(description = "是否为主键") + @NotNull(message = "数据验证失败,是否为主键不能为空!") + private Boolean primaryKey; + + /** + * 是否是自增主键(0: 不是 1: 是)。 + */ + @Schema(description = "是否是自增主键") + @NotNull(message = "数据验证失败,是否是自增主键(0: 不是 1: 是)不能为空!") + private Boolean autoIncrement; + + /** + * 是否可以为空 (0: 不可以为空 1: 可以为空)。 + */ + @Schema(description = "是否可以为空") + @NotNull(message = "数据验证失败,是否可以为空 (0: 不可以为空 1: 可以为空)不能为空!") + private Boolean nullable; + + /** + * 缺省值。 + */ + @Schema(description = "缺省值") + private String columnDefault; + + /** + * 字段在数据表中的显示位置。 + */ + @Schema(description = "字段在数据表中的显示位置") + @NotNull(message = "数据验证失败,字段在数据表中的显示位置不能为空!") + private Integer columnShowOrder; + + /** + * 数据表中的字段注释。 + */ + @Schema(description = "数据表中的字段注释") + private String columnComment; + + /** + * 对象映射字段名称。 + */ + @Schema(description = "对象映射字段名称") + @NotBlank(message = "数据验证失败,对象映射字段名称不能为空!") + private String objectFieldName; + + /** + * 对象映射字段类型。 + */ + @Schema(description = "对象映射字段类型") + @NotBlank(message = "数据验证失败,对象映射字段类型不能为空!") + private String objectFieldType; + + /** + * 数值型字段的精度(目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的精度") + private Integer numericPrecision; + + /** + * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的刻度") + private Integer numericScale; + + /** + * 过滤类型字段。 + */ + @Schema(description = "过滤类型字段") + @NotNull(message = "数据验证失败,过滤类型字段不能为空!", groups = {UpdateGroup.class}) + @ConstDictRef(constDictClass = FieldFilterType.class, message = "数据验证失败,过滤类型字段为无效值!") + private Integer filterType; + + /** + * 是否是主键的父Id。 + */ + @Schema(description = "是否是主键的父Id") + @NotNull(message = "数据验证失败,是否是主键的父Id不能为空!") + private Boolean parentKey; + + /** + * 是否部门过滤字段。 + */ + @Schema(description = "是否部门过滤字段") + @NotNull(message = "数据验证失败,是否部门过滤字段标记不能为空!") + private Boolean deptFilter; + + /** + * 是否用户过滤字段。 + */ + @Schema(description = "是否用户过滤字段") + @NotNull(message = "数据验证失败,是否用户过滤字段标记不能为空!") + private Boolean userFilter; + + /** + * 字段类别。 + */ + @Schema(description = "字段类别") + @ConstDictRef(constDictClass = FieldKind.class, message = "数据验证失败,字段类别为无效值!") + private Integer fieldKind; + + /** + * 包含的文件文件数量,0表示无限制。 + */ + @Schema(description = "包含的文件文件数量,0表示无限制") + private Integer maxFileCount; + + /** + * 上传文件系统类型。 + */ + @Schema(description = "上传文件系统类型") + private Integer uploadFileSystemType; + + /** + * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 + */ + @Schema(description = "脱敏字段类型") + private String maskFieldType; + + /** + * 编码规则的JSON格式数据。 + */ + @Schema(description = "编码规则的JSON格式数据") + private String encodedRule; + + /** + * 字典Id。 + */ + @Schema(description = "字典Id") + private Long dictId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java new file mode 100644 index 00000000..d6789157 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineColumnRuleDto.java @@ -0,0 +1,38 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表字段规则和字段多对多关联Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段规则和字段多对多关联Dto对象") +@Data +public class OnlineColumnRuleDto { + + /** + * 字段Id。 + */ + @Schema(description = "字段Id") + @NotNull(message = "数据验证失败,字段Id不能为空!", groups = {UpdateGroup.class}) + private Long columnId; + + /** + * 规则Id。 + */ + @Schema(description = "规则Id") + @NotNull(message = "数据验证失败,规则Id不能为空!", groups = {UpdateGroup.class}) + private Long ruleId; + + /** + * 规则属性数据。 + */ + @Schema(description = "规则属性数据") + private String propDataJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java new file mode 100644 index 00000000..0fbb006d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceDto.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单的数据源Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源Dto对象") +@Data +public class OnlineDatasourceDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long datasourceId; + + /** + * 数据源名称。 + */ + @Schema(description = "数据源名称") + @NotBlank(message = "数据验证失败,数据源名称不能为空!") + private String datasourceName; + + /** + * 数据源变量名,会成为数据访问url的一部分。 + */ + @Schema(description = "数据源变量名,会成为数据访问url的一部分") + @NotBlank(message = "数据验证失败,数据源变量名不能为空!") + private String variableName; + + /** + * 主表所在的数据库链接Id。 + */ + @Schema(description = "主表所在的数据库链接Id") + @NotNull(message = "数据验证失败,数据库链接Id不能为空!") + private Long dblinkId; + + /** + * 主表Id。 + */ + @Schema(description = "主表Id") + @NotNull(message = "数据验证失败,主表Id不能为空!", groups = {UpdateGroup.class}) + private Long masterTableId; + + /** + * 主表表名。 + */ + @Schema(description = "主表表名") + @NotBlank(message = "数据验证失败,主表名不能为空!", groups = {AddGroup.class}) + private String masterTableName; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java new file mode 100644 index 00000000..3ad19465 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDatasourceRelationDto.java @@ -0,0 +1,107 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.AddGroup; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.RelationType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单的数据源关联Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源关联Dto对象") +@Data +public class OnlineDatasourceRelationDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long relationId; + + /** + * 关联名称。 + */ + @Schema(description = "关联名称") + @NotBlank(message = "数据验证失败,关联名称不能为空!") + private String relationName; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + @NotBlank(message = "数据验证失败,变量名不能为空!") + private String variableName; + + /** + * 主数据源Id。 + */ + @Schema(description = "主数据源Id") + @NotNull(message = "数据验证失败,主数据源Id不能为空!") + private Long datasourceId; + + /** + * 关联类型。 + */ + @Schema(description = "关联类型") + @NotNull(message = "数据验证失败,关联类型不能为空!") + @ConstDictRef(constDictClass = RelationType.class, message = "数据验证失败,关联类型为无效值!") + private Integer relationType; + + /** + * 主表关联字段Id。 + */ + @Schema(description = "主表关联字段Id") + @NotNull(message = "数据验证失败,主表关联字段Id不能为空!") + private Long masterColumnId; + + /** + * 从表Id。 + */ + @Schema(description = "从表Id") + @NotNull(message = "数据验证失败,从表Id不能为空!", groups = {UpdateGroup.class}) + private Long slaveTableId; + + /** + * 从表名。 + */ + @Schema(description = "从表名") + @NotBlank(message = "数据验证失败,从表名不能为空!", groups = {AddGroup.class}) + private String slaveTableName; + + /** + * 从表关联字段Id。 + */ + @Schema(description = "从表关联字段Id") + @NotNull(message = "数据验证失败,从表关联字段Id不能为空!", groups = {UpdateGroup.class}) + private Long slaveColumnId; + + /** + * 从表字段名。 + */ + @Schema(description = "从表字段名") + @NotBlank(message = "数据验证失败,从表字段名不能为空!", groups = {AddGroup.class}) + private String slaveColumnName; + + /** + * 是否级联删除标记。 + */ + @Schema(description = "是否级联删除标记") + @NotNull(message = "数据验证失败,是否级联删除标记不能为空!") + private Boolean cascadeDelete; + + /** + * 是否左连接标记。 + */ + @Schema(description = "是否左连接标记") + @NotNull(message = "数据验证失败,是否左连接标记不能为空!") + private Boolean leftJoin; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java new file mode 100644 index 00000000..2e1f2488 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDblinkDto.java @@ -0,0 +1,53 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表所在数据库链接Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表所在数据库链接Dto对象") +@Data +public class OnlineDblinkDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long dblinkId; + + /** + * 链接中文名称。 + */ + @Schema(description = "链接中文名称") + @NotBlank(message = "数据验证失败,链接中文名称不能为空!") + private String dblinkName; + + /** + * 链接描述。 + */ + @Schema(description = "链接中文名称") + private String dblinkDescription; + + /** + * 配置信息。 + */ + @Schema(description = "配置信息") + @NotBlank(message = "数据验证失败,配置信息不能为空!") + private String configuration; + + /** + * 数据库链接类型。 + */ + @Schema(description = "数据库链接类型") + @NotNull(message = "数据验证失败,数据库链接类型不能为空!") + private Integer dblinkType; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java new file mode 100644 index 00000000..f25444ce --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineDictDto.java @@ -0,0 +1,128 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.core.constant.DictType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单关联的字典Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单关联的字典Dto对象") +@Data +public class OnlineDictDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long dictId; + + /** + * 字典名称。 + */ + @Schema(description = "字典名称") + @NotBlank(message = "数据验证失败,字典名称不能为空!") + private String dictName; + + /** + * 字典类型。 + */ + @Schema(description = "字典类型") + @NotNull(message = "数据验证失败,字典类型不能为空!") + @ConstDictRef(constDictClass = DictType.class, message = "数据验证失败,字典类型为无效值!") + private Integer dictType; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 字典表名称。 + */ + @Schema(description = "字典表名称") + private String tableName; + + /** + * 全局字典编码。 + */ + @Schema(description = "全局字典编码") + private String dictCode; + + /** + * 字典表键字段名称。 + */ + @Schema(description = "字典表键字段名称") + private String keyColumnName; + + /** + * 字典表父键字段名称。 + */ + @Schema(description = "字典表父键字段名称") + private String parentKeyColumnName; + + /** + * 字典值字段名称。 + */ + @Schema(description = "字典值字段名称") + private String valueColumnName; + + /** + * 逻辑删除字段。 + */ + @Schema(description = "逻辑删除字段") + private String deletedColumnName; + + /** + * 用户过滤滤字段名称。 + */ + @Schema(description = "用户过滤滤字段名称") + private String userFilterColumnName; + + /** + * 部门过滤字段名称。 + */ + @Schema(description = "部门过滤字段名称") + private String deptFilterColumnName; + + /** + * 租户过滤字段名称。 + */ + @Schema(description = "租户过滤字段名称") + private String tenantFilterColumnName; + + /** + * 获取字典数据的url。 + */ + @Schema(description = "获取字典数据的url") + private String dictListUrl; + + /** + * 根据主键id批量获取字典数据的url。 + */ + @Schema(description = "根据主键id批量获取字典数据的url") + private String dictIdsUrl; + + /** + * 字典的JSON数据。 + */ + @Schema(description = "字典的JSON数据") + private String dictDataJson; + + /** + * 是否树形标记。 + */ + @Schema(description = "是否树形标记") + @NotNull(message = "数据验证失败,是否树形标记不能为空!") + private Boolean treeFlag; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java new file mode 100644 index 00000000..8d638b90 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFilterDto.java @@ -0,0 +1,72 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.online.model.constant.FieldFilterType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Set; + +/** + * 在线表单数据过滤参数对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据过滤参数对象") +@Data +public class OnlineFilterDto { + + /** + * 表名。 + */ + @Schema(description = "表名") + private String tableName; + + /** + * 过滤字段名。 + */ + @Schema(description = "过滤字段名") + private String columnName; + + /** + * 过滤值。 + */ + @Schema(description = "过滤值") + private Object columnValue; + + /** + * 范围比较的最小值。 + */ + @Schema(description = "范围比较的最小值") + private Object columnValueStart; + + /** + * 范围比较的最大值。 + */ + @Schema(description = "范围比较的最大值") + private Object columnValueEnd; + + /** + * 仅当操作符为IN的时候使用。 + */ + @Schema(description = "仅当操作符为IN的时候使用") + private Set columnValueList; + + /** + * 过滤类型,参考FieldFilterType常量对象。缺省值就是等于过滤了。 + */ + @Schema(description = "过滤类型") + private Integer filterType = FieldFilterType.EQUAL_FILTER; + + /** + * 是否为字典多选。 + */ + @Schema(description = "是否为字典多选") + private Boolean dictMultiSelect = false; + + /** + * 是否为Oracle的日期类型。 + */ + private Boolean isOracleDate = false; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java new file mode 100644 index 00000000..2abcde8c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineFormDto.java @@ -0,0 +1,91 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.FormKind; +import com.orangeforms.common.online.model.constant.FormType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * 在线表单Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单Dto对象") +@Data +public class OnlineFormDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long formId; + + /** + * 页面id。 + */ + @Schema(description = "页面id") + @NotNull(message = "数据验证失败,页面id不能为空!") + private Long pageId; + + /** + * 表单编码。 + */ + @Schema(description = "表单编码") + private String formCode; + + /** + * 表单名称。 + */ + @Schema(description = "表单名称") + @NotBlank(message = "数据验证失败,表单名称不能为空!") + private String formName; + + /** + * 表单类别。 + */ + @Schema(description = "表单类别") + @NotNull(message = "数据验证失败,表单类别不能为空!") + @ConstDictRef(constDictClass = FormKind.class, message = "数据验证失败,表单类别为无效值!") + private Integer formKind; + + /** + * 表单类型。 + */ + @Schema(description = "表单类型") + @NotNull(message = "数据验证失败,表单类型不能为空!") + @ConstDictRef(constDictClass = FormType.class, message = "数据验证失败,表单类型为无效值!") + private Integer formType; + + /** + * 表单主表id。 + */ + @Schema(description = "表单主表id") + @NotNull(message = "数据验证失败,表单主表id不能为空!") + private Long masterTableId; + + /** + * 当前表单关联的数据源Id集合。 + */ + @Schema(description = "当前表单关联的数据源Id集合") + private List datasourceIdList; + + /** + * 表单组件JSON。 + */ + @Schema(description = "表单组件JSON") + private String widgetJson; + + /** + * 表单参数JSON。 + */ + @Schema(description = "表单参数JSON") + private String paramsJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java new file mode 100644 index 00000000..e6a3c3c3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDatasourceDto.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单页面和数据源多对多关联Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单页面和数据源多对多关联Dto对象") +@Data +public class OnlinePageDatasourceDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long id; + + /** + * 页面主键Id。 + */ + @Schema(description = "页面主键Id") + @NotNull(message = "数据验证失败,页面主键Id不能为空!") + private Long pageId; + + /** + * 数据源主键Id。 + */ + @Schema(description = "数据源主键Id") + @NotNull(message = "数据验证失败,数据源主键Id不能为空!") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java new file mode 100644 index 00000000..309c3bf4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlinePageDto.java @@ -0,0 +1,58 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.model.constant.PageType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单所在页面Dto对象。这里我们可以把页面理解为表单的容器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单所在页面Dto对象") +@Data +public class OnlinePageDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long pageId; + + /** + * 页面编码。 + */ + @Schema(description = "页面编码") + private String pageCode; + + /** + * 页面名称。 + */ + @Schema(description = "页面名称") + @NotBlank(message = "数据验证失败,页面名称不能为空!") + private String pageName; + + /** + * 页面类型。 + */ + @Schema(description = "页面类型") + @NotNull(message = "数据验证失败,页面类型不能为空!") + @ConstDictRef(constDictClass = PageType.class, message = "数据验证失败,页面类型为无效值!") + private Integer pageType; + + /** + * 页面编辑状态。 + */ + @Schema(description = "页面编辑状态") + @NotNull(message = "数据验证失败,状态不能为空!") + @ConstDictRef(constDictClass = PageStatus.class, message = "数据验证失败,状态为无效值!") + private Integer status; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java new file mode 100644 index 00000000..e89517c0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineRuleDto.java @@ -0,0 +1,56 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import com.orangeforms.common.online.model.constant.RuleType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单数据表字段验证规则Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段验证规则Dto对象") +@Data +public class OnlineRuleDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long ruleId; + + /** + * 规则名称。 + */ + @Schema(description = "规则名称") + @NotBlank(message = "数据验证失败,规则名称不能为空!") + private String ruleName; + + /** + * 规则类型。 + */ + @Schema(description = "规则类型") + @NotNull(message = "数据验证失败,规则类型不能为空!") + @ConstDictRef(constDictClass = RuleType.class, message = "数据验证失败,规则类型为无效值!") + private Integer ruleType; + + /** + * 内置规则标记。 + */ + @Schema(description = "内置规则标记") + @NotNull(message = "数据验证失败,内置规则标记不能为空!") + private Boolean builtin; + + /** + * 自定义规则的正则表达式。 + */ + @Schema(description = "自定义规则的正则表达式") + private String pattern; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java new file mode 100644 index 00000000..774f985b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineTableDto.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 在线表单的数据表Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据表Dto对象") +@Data +public class OnlineTableDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long tableId; + + /** + * 表名称。 + */ + @Schema(description = "表名称") + @NotBlank(message = "数据验证失败,表名称不能为空!") + private String tableName; + + /** + * 实体名称。 + */ + @Schema(description = "实体名称") + @NotBlank(message = "数据验证失败,实体名称不能为空!") + private String modelName; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + @NotNull(message = "数据验证失败,数据库链接Id不能为空!") + private Long dblinkId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java new file mode 100644 index 00000000..040850de --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/dto/OnlineVirtualColumnDto.java @@ -0,0 +1,102 @@ +package com.orangeforms.common.online.dto; + +import com.orangeforms.common.core.constant.AggregationType; +import com.orangeforms.common.core.validator.ConstDictRef; +import com.orangeforms.common.core.validator.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; + +import com.orangeforms.common.online.model.constant.VirtualType; +import lombok.Data; + +import jakarta.validation.constraints.*; + +/** + * 在线数据表虚拟字段Dto对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线数据表虚拟字段Dto对象") +@Data +public class OnlineVirtualColumnDto { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + private Long virtualColumnId; + + /** + * 所在表Id。 + */ + @Schema(description = "所在表Id") + private Long tableId; + + /** + * 字段名称。 + */ + @Schema(description = "字段名称") + @NotBlank(message = "数据验证失败,字段名称不能为空!") + private String objectFieldName; + + /** + * 属性类型。 + */ + @Schema(description = "属性类型") + @NotBlank(message = "数据验证失败,属性类型不能为空!") + private String objectFieldType; + + /** + * 字段提示名。 + */ + @Schema(description = "字段提示名") + @NotBlank(message = "数据验证失败,字段提示名不能为空!") + private String columnPrompt; + + /** + * 虚拟字段类型(0: 聚合)。 + */ + @Schema(description = "虚拟字段类型(0: 聚合)") + @ConstDictRef(constDictClass = VirtualType.class, message = "数据验证失败,虚拟字段类型为无效值!") + @NotNull(message = "数据验证失败,虚拟字段类型(0: 聚合)不能为空!") + private Integer virtualType; + + /** + * 关联数据源Id。 + */ + @Schema(description = "关联数据源Id") + @NotNull(message = "数据验证失败,关联数据源Id不能为空!") + private Long datasourceId; + + /** + * 关联Id。 + */ + @Schema(description = "关联Id") + private Long relationId; + + /** + * 聚合字段所在关联表Id。 + */ + @Schema(description = "聚合字段所在关联表Id") + private Long aggregationTableId; + + /** + * 关联表聚合字段Id。 + */ + @Schema(description = "关联表聚合字段Id") + private Long aggregationColumnId; + + /** + * 聚合类型(0: sum 1: count 2: avg 3: min 4: max)。 + */ + @Schema(description = "聚合类型(0: sum 1: count 2: avg 3: min 4: max)") + @ConstDictRef(constDictClass = AggregationType.class, message = "数据验证失败,虚拟字段聚合计算类型为无效值!") + private Integer aggregationType; + + /** + * 存储过滤条件的json。 + */ + @Schema(description = "存储过滤条件的json") + private String whereClauseJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java new file mode 100644 index 00000000..a2ac52f2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/exception/OnlineRuntimeException.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.online.exception; + +import com.orangeforms.common.core.exception.MyRuntimeException; + +/** + * 在线表单运行时异常。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineRuntimeException extends MyRuntimeException { + + /** + * 构造函数。 + */ + public OnlineRuntimeException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public OnlineRuntimeException(String msg) { + super(msg); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java new file mode 100644 index 00000000..fd2466c4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumn.java @@ -0,0 +1,215 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationOneToOne; +import com.orangeforms.common.online.model.constant.FieldKind; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_column") +public class OnlineColumn { + + /** + * 主键Id。 + */ + @TableId(value = "column_id") + private Long columnId; + + /** + * 字段名。 + */ + @TableField(value = "column_name") + private String columnName; + + /** + * 数据表Id。 + */ + @TableField(value = "table_id") + private Long tableId; + + /** + * 数据表中的字段类型。 + */ + @TableField(value = "column_type") + private String columnType; + + /** + * 数据表中的完整字段类型(包括了精度和刻度)。 + */ + @TableField(value = "full_column_type") + private String fullColumnType; + + /** + * 是否为主键。 + */ + @TableField(value = "primary_key") + private Boolean primaryKey; + + /** + * 是否是自增主键(0: 不是 1: 是)。 + */ + @TableField(value = "auto_incr") + private Boolean autoIncrement; + + /** + * 是否可以为空 (0: 不可以为空 1: 可以为空)。 + */ + @TableField(value = "nullable") + private Boolean nullable; + + /** + * 缺省值。 + */ + @TableField(value = "column_default") + private String columnDefault; + + /** + * 字段在数据表中的显示位置。 + */ + @TableField(value = "column_show_order") + private Integer columnShowOrder; + + /** + * 数据表中的字段注释。 + */ + @TableField(value = "column_comment") + private String columnComment; + + /** + * 对象映射字段名称。 + */ + @TableField(value = "object_field_name") + private String objectFieldName; + + /** + * 对象映射字段类型。 + */ + @TableField(value = "object_field_type") + private String objectFieldType; + + /** + * 数值型字段的精度(目前仅Oracle使用)。 + */ + @TableField(value = "numeric_precision") + private Integer numericPrecision; + + /** + * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 + */ + @TableField(value = "numeric_scale") + private Integer numericScale; + + /** + * 过滤字段类型。 + */ + @TableField(value = "filter_type") + private Integer filterType; + + /** + * 是否是主键的父Id。 + */ + @TableField(value = "parent_key") + private Boolean parentKey; + + /** + * 是否部门过滤字段。 + */ + @TableField(value = "dept_filter") + private Boolean deptFilter; + + /** + * 是否用户过滤字段。 + */ + @TableField(value = "user_filter") + private Boolean userFilter; + + /** + * 字段类别。 + */ + @TableField(value = "field_kind") + private Integer fieldKind; + + /** + * 包含的文件文件数量,0表示无限制。 + */ + @TableField(value = "max_file_count") + private Integer maxFileCount; + + /** + * 上传文件系统类型。 + */ + @TableField(value = "upload_file_system_type") + private Integer uploadFileSystemType; + + /** + * 编码规则的JSON格式数据。 + */ + @TableField(value = "encoded_rule") + private String encodedRule; + + /** + * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 + */ + @TableField(value = "mask_field_type") + private String maskFieldType; + + /** + * 字典Id。 + */ + @TableField(value = "dict_id") + private Long dictId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * SQL查询时候使用的别名。 + */ + @TableField(exist = false) + private String columnAliasName; + + @RelationConstDict( + masterIdField = "fieldKind", + constantDictClass = FieldKind.class) + @TableField(exist = false) + private Map fieldKindDictMap; + + @RelationOneToOne( + masterIdField = "dictId", + slaveModelClass = OnlineDict.class, + slaveIdField = "dictId", + loadSlaveDict = false) + @TableField(exist = false) + private OnlineDict dictInfo; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java new file mode 100644 index 00000000..f89876e4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineColumnRule.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 在线表单数据表字段规则和字段多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_column_rule") +public class OnlineColumnRule { + + /** + * 字段Id。 + */ + @TableField(value = "column_id") + private Long columnId; + + /** + * 规则Id。 + */ + @TableField(value = "rule_id") + private Long ruleId; + + /** + * 规则属性数据。 + */ + @TableField(value = "prop_data_json") + private String propDataJson; + + @TableField(exist = false) + private OnlineRule onlineRule; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java new file mode 100644 index 00000000..e7b16a9c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasource.java @@ -0,0 +1,103 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationDict; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单的数据源实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_datasource") +public class OnlineDatasource { + + /** + * 主键Id。 + */ + @TableId(value = "datasource_id") + private Long datasourceId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 数据源名称。 + */ + @TableField(value = "datasource_name") + private String datasourceName; + + /** + * 数据源变量名,会成为数据访问url的一部分。 + */ + @TableField(value = "variable_name") + private String variableName; + + /** + * 数据库链接Id。 + */ + @TableField(value = "dblink_id") + private Long dblinkId; + + /** + * 主表Id。 + */ + @TableField(value = "master_table_id") + private Long masterTableId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * datasourceId 的多对多关联的数据对象。 + */ + @TableField(exist = false) + private OnlinePageDatasource onlinePageDatasource; + + /** + * datasourceId 的多对多关联的数据对象。 + */ + @TableField(exist = false) + private List onlineFormDatasourceList; + + @RelationDict( + masterIdField = "masterTableId", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId", + slaveNameField = "tableName") + @TableField(exist = false) + private Map masterTableIdDictMap; + + @TableField(exist = false) + private OnlineTable masterTable; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java new file mode 100644 index 00000000..75161e59 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceRelation.java @@ -0,0 +1,166 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationDict; +import com.orangeforms.common.core.annotation.RelationOneToOne; +import com.orangeforms.common.online.model.constant.RelationType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单的数据源关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_datasource_relation") +public class OnlineDatasourceRelation { + + /** + * 主键Id。 + */ + @TableId(value = "relation_id") + private Long relationId; + + /** + * 应用Id。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 关联名称。 + */ + @TableField(value = "relation_name") + private String relationName; + + /** + * 变量名。 + */ + @TableField(value = "variable_name") + private String variableName; + + /** + * 主数据源Id。 + */ + @TableField(value = "datasource_id") + private Long datasourceId; + + /** + * 关联类型。 + */ + @TableField(value = "relation_type") + private Integer relationType; + + /** + * 主表关联字段Id。 + */ + @TableField(value = "master_column_id") + private Long masterColumnId; + + /** + * 从表Id。 + */ + @TableField(value = "slave_table_id") + private Long slaveTableId; + + /** + * 从表关联字段Id。 + */ + @TableField(value = "slave_column_id") + private Long slaveColumnId; + + /** + * 删除主表的时候是否级联删除一对一和一对多的从表数据,多对多只是删除关联,不受到这个标记的影响。。 + */ + @TableField(value = "cascade_delete") + private Boolean cascadeDelete; + + /** + * 是否左连接。 + */ + @TableField(value = "left_join") + private Boolean leftJoin; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + @RelationOneToOne( + masterIdField = "masterColumnId", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId") + @TableField(exist = false) + private OnlineColumn masterColumn; + + @RelationOneToOne( + masterIdField = "slaveTableId", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId") + @TableField(exist = false) + private OnlineTable slaveTable; + + @RelationOneToOne( + masterIdField = "slaveColumnId", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId") + @TableField(exist = false) + private OnlineColumn slaveColumn; + + @RelationDict( + masterIdField = "masterColumnId", + equalOneToOneRelationField = "onlineColumn", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId", + slaveNameField = "columnName") + @TableField(exist = false) + private Map masterColumnIdDictMap; + + @RelationDict( + masterIdField = "slaveTableId", + equalOneToOneRelationField = "onlineTable", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId", + slaveNameField = "modelName") + @TableField(exist = false) + private Map slaveTableIdDictMap; + + @RelationDict( + masterIdField = "slaveColumnId", + equalOneToOneRelationField = "onlineColumn", + slaveModelClass = OnlineColumn.class, + slaveIdField = "columnId", + slaveNameField = "columnName") + @TableField(exist = false) + private Map slaveColumnIdDictMap; + + @RelationConstDict( + masterIdField = "relationType", + constantDictClass = RelationType.class) + @TableField(exist = false) + private Map relationTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java new file mode 100644 index 00000000..d4ea5d92 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDatasourceTable.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 数据源及其关联所引用的数据表实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_datasource_table") +public class OnlineDatasourceTable { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 数据源Id。 + */ + @TableField(value = "datasource_id") + private Long datasourceId; + + /** + * 数据源关联Id。 + */ + @TableField(value = "relation_id") + private Long relationId; + + /** + * 数据表Id。 + */ + @TableField(value = "table_id") + private Long tableId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java new file mode 100644 index 00000000..635cbe6d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDblink.java @@ -0,0 +1,85 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.dbutil.constant.DblinkType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表所在数据库链接实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_dblink") +public class OnlineDblink { + + /** + * 主键Id。 + */ + @TableId(value = "dblink_id") + private Long dblinkId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 链接中文名称。 + */ + @TableField(value = "dblink_name") + private String dblinkName; + + /** + * 链接描述。 + */ + @TableField(value = "dblink_description") + private String dblinkDescription; + + /** + * 配置信息。 + */ + private String configuration; + + /** + * 数据库链接类型。 + */ + @TableField(value = "dblink_type") + private Integer dblinkType; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 修改时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + @RelationConstDict( + masterIdField = "dblinkType", + constantDictClass = DblinkType.class) + @TableField(exist = false) + private Map dblinkTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java new file mode 100644 index 00000000..533995c5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineDict.java @@ -0,0 +1,167 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.core.annotation.RelationDict; +import com.orangeforms.common.core.constant.DictType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单关联的字典实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_dict") +public class OnlineDict { + + /** + * 主键Id。 + */ + @TableId(value = "dict_id") + private Long dictId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 字典名称。 + */ + @TableField(value = "dict_name") + private String dictName; + + /** + * 字典类型。 + */ + @TableField(value = "dict_type") + private Integer dictType; + + /** + * 数据库链接Id。 + */ + @TableField(value = "dblink_id") + private Long dblinkId; + + /** + * 字典表名称。 + */ + @TableField(value = "table_name") + private String tableName; + + /** + * 全局字典编码。 + */ + @TableField(value = "dict_code") + private String dictCode; + + /** + * 字典表键字段名称。 + */ + @TableField(value = "key_column_name") + private String keyColumnName; + + /** + * 字典表父键字段名称。 + */ + @TableField(value = "parent_key_column_name") + private String parentKeyColumnName; + + /** + * 字典值字段名称。 + */ + @TableField(value = "value_column_name") + private String valueColumnName; + + /** + * 逻辑删除字段。 + */ + @TableField(value = "deleted_column_name") + private String deletedColumnName; + + /** + * 用户过滤滤字段名称。 + */ + @TableField(value = "user_filter_column_name") + private String userFilterColumnName; + + /** + * 部门过滤字段名称。 + */ + @TableField(value = "dept_filter_column_name") + private String deptFilterColumnName; + + /** + * 租户过滤字段名称。 + */ + @TableField(value = "tenant_filter_column_name") + private String tenantFilterColumnName; + + /** + * 是否树形标记。 + */ + @TableField(value = "tree_flag") + private Boolean treeFlag; + + /** + * 获取字典数据的url。 + */ + @TableField(value = "dict_list_url") + private String dictListUrl; + + /** + * 根据主键id批量获取字典数据的url。 + */ + @TableField(value = "dict_ids_url") + private String dictIdsUrl; + + /** + * 字典的JSON数据。 + */ + @TableField(value = "dict_data_json") + private String dictDataJson; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + @RelationConstDict( + masterIdField = "dictType", + constantDictClass = DictType.class) + @TableField(exist = false) + private Map dictTypeDictMap; + + @RelationDict( + masterIdField = "dblinkId", + slaveModelClass = OnlineDblink.class, + slaveIdField = "dblinkId", + slaveNameField = "dblinkName") + @TableField(exist = false) + private Map dblinkIdDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java new file mode 100644 index 00000000..5a4c9a12 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineForm.java @@ -0,0 +1,132 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.*; +import com.orangeforms.common.online.model.constant.FormType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_form") +public class OnlineForm { + + /** + * 主键Id。 + */ + @TableId(value = "form_id") + private Long formId; + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 页面id。 + */ + @TableField(value = "page_id") + private Long pageId; + + /** + * 表单编码。 + */ + @TableField(value = "form_code") + private String formCode; + + /** + * 表单名称。 + */ + @TableField(value = "form_name") + private String formName; + + /** + * 表单类别。 + */ + @TableField(value = "form_kind") + private Integer formKind; + + /** + * 表单类型。 + */ + @TableField(value = "form_type") + private Integer formType; + + /** + * 表单主表id。 + */ + @TableField(value = "master_table_id") + private Long masterTableId; + + /** + * 表单组件JSON。 + */ + @TableField(value = "widget_json") + private String widgetJson; + + /** + * 表单参数JSON。 + */ + @TableField(value = "params_json") + private String paramsJson; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + @RelationOneToOne( + masterIdField = "masterTableId", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId") + @TableField(exist = false) + private OnlineTable onlineTable; + + @RelationDict( + masterIdField = "masterTableId", + equalOneToOneRelationField = "onlineTable", + slaveModelClass = OnlineTable.class, + slaveIdField = "tableId", + slaveNameField = "modelName") + @TableField(exist = false) + private Map masterTableIdDictMap; + + @RelationConstDict( + masterIdField = "formType", + constantDictClass = FormType.class) + @TableField(exist = false) + private Map formTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java new file mode 100644 index 00000000..d1ddef7f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineFormDatasource.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 在线表单和数据源多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_form_datasource") +public class OnlineFormDatasource { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 表单Id。 + */ + @TableField(value = "form_id") + private Long formId; + + /** + * 数据源Id。 + */ + @TableField(value = "datasource_id") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java new file mode 100644 index 00000000..f39e9448 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePage.java @@ -0,0 +1,105 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.model.constant.PageType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单所在页面实体对象。这里我们可以把页面理解为表单的容器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_page") +public class OnlinePage { + + /** + * 主键Id。 + */ + @TableId(value = "page_id") + private Long pageId; + + /** + * 租户Id。 + */ + @TableField(value = "tenant_id") + private Long tenantId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 页面编码。 + */ + @TableField(value = "page_code") + private String pageCode; + + /** + * 页面名称。 + */ + @TableField(value = "page_name") + private String pageName; + + /** + * 页面类型。 + */ + @TableField(value = "page_type") + private Integer pageType; + + /** + * 页面编辑状态。 + */ + @TableField(value = "status") + private Integer status; + + /** + * 是否发布。 + */ + @TableField(value = "published") + private Boolean published; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + @RelationConstDict( + masterIdField = "pageType", + constantDictClass = PageType.class) + @TableField(exist = false) + private Map pageTypeDictMap; + + @RelationConstDict( + masterIdField = "status", + constantDictClass = PageStatus.class) + @TableField(exist = false) + private Map statusDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java new file mode 100644 index 00000000..b710cec7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlinePageDatasource.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 在线表单页面和数据源多对多关联实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_page_datasource") +public class OnlinePageDatasource { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 页面主键Id。 + */ + @TableField(value = "page_id") + private Long pageId; + + /** + * 数据源主键Id。 + */ + @TableField(value = "datasource_id") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java new file mode 100644 index 00000000..289adfb3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineRule.java @@ -0,0 +1,99 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationConstDict; +import com.orangeforms.common.online.model.constant.RuleType; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段验证规则实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_rule") +public class OnlineRule { + + /** + * 主键Id。 + */ + @TableId(value = "rule_id") + private Long ruleId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 规则名称。 + */ + @TableField(value = "rule_name") + private String ruleName; + + /** + * 规则类型。 + */ + @TableField(value = "rule_type") + private Integer ruleType; + + /** + * 内置规则标记。 + */ + @TableField(value = "builtin") + private Boolean builtin; + + /** + * 自定义规则的正则表达式。 + */ + @TableField(value = "pattern") + private String pattern; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @TableLogic + @TableField(value = "deleted_flag") + private Integer deletedFlag; + + /** + * ruleId 的多对多关联表数据对象。 + */ + @TableField(exist = false) + private OnlineColumnRule onlineColumnRule; + + @RelationConstDict( + masterIdField = "ruleType", + constantDictClass = RuleType.class) + @TableField(exist = false) + private Map ruleTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java new file mode 100644 index 00000000..ed5e2297 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineTable.java @@ -0,0 +1,99 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import com.orangeforms.common.core.annotation.RelationOneToMany; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单的数据表实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_table") +public class OnlineTable { + + /** + * 主键Id。 + */ + @TableId(value = "table_id") + private Long tableId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @TableField(value = "app_code") + private String appCode; + + /** + * 表名称。 + */ + @TableField(value = "table_name") + private String tableName; + + /** + * 实体名称。 + */ + @TableField(value = "model_name") + private String modelName; + + /** + * 数据库链接Id。 + */ + @TableField(value = "dblink_id") + private Long dblinkId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 更新者。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + @RelationOneToMany( + masterIdField = "tableId", + slaveModelClass = OnlineColumn.class, + slaveIdField = "tableId") + @TableField(exist = false) + private List columnList; + + /** + * 该字段会被缓存,因此在线表单执行操作时可以从缓存中读取该数据,并可基于columnId进行快速检索。 + */ + @TableField(exist = false) + private Map columnMap; + + /** + * 当前表的主键字段,该字段仅仅用于动态表单运行时的SQL拼装。 + */ + @TableField(exist = false) + private OnlineColumn primaryKeyColumn; + + /** + * 当前表的逻辑删除字段,该字段仅仅用于动态表单运行时的SQL拼装。 + */ + @TableField(exist = false) + private OnlineColumn logicDeleteColumn; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java new file mode 100644 index 00000000..f7e12374 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/OnlineVirtualColumn.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.online.model; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +/** + * 在线数据表虚拟字段实体对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@TableName(value = "zz_online_virtual_column") +public class OnlineVirtualColumn { + + /** + * 主键Id。 + */ + @TableId(value = "virtual_column_id") + private Long virtualColumnId; + + /** + * 所在表Id。 + */ + @TableField(value = "table_id") + private Long tableId; + + /** + * 字段名称。 + */ + @TableField(value = "object_field_name") + private String objectFieldName; + + /** + * 属性类型。 + */ + @TableField(value = "object_field_type") + private String objectFieldType; + + /** + * 字段提示名。 + */ + @TableField(value = "column_prompt") + private String columnPrompt; + + /** + * 虚拟字段类型(0: 聚合)。 + */ + @TableField(value = "virtual_type") + private Integer virtualType; + + /** + * 关联数据源Id。 + */ + @TableField(value = "datasource_id") + private Long datasourceId; + + /** + * 关联Id。 + */ + @TableField(value = "relation_id") + private Long relationId; + + /** + * 聚合字段所在关联表Id。 + */ + @TableField(value = "aggregation_table_id") + private Long aggregationTableId; + + /** + * 关联表聚合字段Id。 + */ + @TableField(value = "aggregation_column_id") + private Long aggregationColumnId; + + /** + * 聚合类型(0: count 1: sum 2: avg 3: max 4:min)。 + */ + @TableField(value = "aggregation_type") + private Integer aggregationType; + + /** + * 存储过滤条件的json。 + */ + @TableField(value = "where_clause_json") + private String whereClauseJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java new file mode 100644 index 00000000..6287a355 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldFilterType.java @@ -0,0 +1,79 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FieldFilterType { + + /** + * 无过滤。 + */ + public static final int NO_FILTER = 0; + /** + * 等于过滤。 + */ + public static final int EQUAL_FILTER = 1; + /** + * 范围过滤。 + */ + public static final int RANGE_FILTER = 2; + /** + * 模糊过滤。 + */ + public static final int LIKE_FILTER = 3; + /** + * IN LIST列表过滤。 + */ + public static final int IN_LIST_FILTER = 4; + /** + * 用OR连接的多个模糊查询。 + */ + public static final int MULTI_LIKE = 5; + /** + * NOT IN LIST列表过滤。 + */ + public static final int NOT_IN_LIST_FILTER = 6; + /** + * NOT IN LIST列表过滤。 + */ + public static final int IS_NULL = 7; + /** + * NOT IN LIST列表过滤。 + */ + public static final int IS_NOT_NULL = 8; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(NO_FILTER, "无过滤"); + DICT_MAP.put(EQUAL_FILTER, "等于过滤"); + DICT_MAP.put(RANGE_FILTER, "范围过滤"); + DICT_MAP.put(LIKE_FILTER, "模糊过滤"); + DICT_MAP.put(IN_LIST_FILTER, "IN LIST列表过滤"); + DICT_MAP.put(MULTI_LIKE, "用OR连接的多个模糊查询"); + DICT_MAP.put(NOT_IN_LIST_FILTER, "NOT IN LIST列表过滤"); + DICT_MAP.put(IS_NULL, "IS NULL"); + DICT_MAP.put(IS_NOT_NULL, "IS NOT NULL"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldFilterType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java new file mode 100644 index 00000000..d8afef0b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FieldKind.java @@ -0,0 +1,109 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段类别常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FieldKind { + + /** + * 文件上传字段。 + */ + public static final int UPLOAD = 1; + /** + * 图片上传字段。 + */ + public static final int UPLOAD_IMAGE = 2; + /** + * 富文本字段。 + */ + public static final int RICH_TEXT = 3; + /** + * 字典多选字段。 + */ + public static final int DICT_MULTI_SELECT = 4; + /** + * 创建人部门Id。 + */ + public static final int CREATE_DEPT_ID = 19; + /** + * 创建时间字段。 + */ + public static final int CREATE_TIME = 20; + /** + * 创建人字段。 + */ + public static final int CREATE_USER_ID = 21; + /** + * 更新时间字段。 + */ + public static final int UPDATE_TIME = 22; + /** + * 更新人字段。 + */ + public static final int UPDATE_USER_ID = 23; + /** + * 包含自动编码。 + */ + public static final int AUTO_CODE = 24; + /** + * 流程最后审批状态。 + */ + public static final int FLOW_APPROVAL_STATUS = 25; + /** + * 流程结束状态。 + */ + public static final int FLOW_FINISHED_STATUS = 26; + /** + * 脱敏字段。 + */ + public static final int MASK_FIELD = 27; + /** + * 租户过滤字段。 + */ + public static final int TENANT_FILTER = 28; + /** + * 逻辑删除字段。 + */ + public static final int LOGIC_DELETE = 31; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(UPLOAD, "文件上传字段"); + DICT_MAP.put(UPLOAD_IMAGE, "图片上传字段"); + DICT_MAP.put(RICH_TEXT, "富文本字段"); + DICT_MAP.put(DICT_MULTI_SELECT, "字典多选字段"); + DICT_MAP.put(CREATE_DEPT_ID, "创建人部门字段"); + DICT_MAP.put(CREATE_TIME, "创建时间字段"); + DICT_MAP.put(CREATE_USER_ID, "创建人字段"); + DICT_MAP.put(UPDATE_TIME, "更新时间字段"); + DICT_MAP.put(UPDATE_USER_ID, "更新人字段"); + DICT_MAP.put(AUTO_CODE, "自动编码字段"); + DICT_MAP.put(FLOW_APPROVAL_STATUS, "流程最后审批状态"); + DICT_MAP.put(FLOW_FINISHED_STATUS, "流程结束状态"); + DICT_MAP.put(MASK_FIELD, "脱敏字段"); + DICT_MAP.put(TENANT_FILTER, "租户过滤字段"); + DICT_MAP.put(LOGIC_DELETE, "逻辑删除字段"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldKind() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java new file mode 100644 index 00000000..71b22651 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormKind.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表单类别常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FormKind { + + /** + * 弹框。 + */ + public static final int DIALOG = 1; + /** + * 跳页。 + */ + public static final int NEW_PAGE = 5; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(DIALOG, "弹框列表"); + DICT_MAP.put(NEW_PAGE, "跳页类别"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FormKind() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java new file mode 100644 index 00000000..6b969c20 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/FormType.java @@ -0,0 +1,64 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表单类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class FormType { + + /** + * 查询表单。 + */ + public static final int QUERY = 1; + /** + * 左树右表表单。 + */ + public static final int ADVANCED_QUERY = 2; + /** + * 一对一关联数据查询。 + */ + public static final int ONE_TO_ONE_QUERY = 3; + /** + * 编辑表单。 + */ + public static final int EDIT_FORM = 5; + /** + * 流程表单。 + */ + public static final int FLOW = 10; + /** + * 流程工单表单。 + */ + public static final int FLOW_WORK_ORDER = 11; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(QUERY, "查询表单"); + DICT_MAP.put(ADVANCED_QUERY, "左树右表表单"); + DICT_MAP.put(ONE_TO_ONE_QUERY, "一对一关联数据查询"); + DICT_MAP.put(EDIT_FORM, "编辑表单"); + DICT_MAP.put(FLOW, "流程表单"); + DICT_MAP.put(FLOW_WORK_ORDER, "流程工单表单"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FormType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java new file mode 100644 index 00000000..6eed451d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageStatus.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 页面状态常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class PageStatus { + + /** + * 编辑基础信息。 + */ + public static final int BASIC = 0; + /** + * 编辑数据模型。 + */ + public static final int DATASOURCE = 1; + /** + * 设计表单。 + */ + public static final int FORM_DESIGN = 2; + + private static final Map DICT_MAP = new HashMap<>(4); + static { + DICT_MAP.put(BASIC, "编辑基础信息"); + DICT_MAP.put(DATASOURCE, "编辑数据模型"); + DICT_MAP.put(FORM_DESIGN, "设计表单"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private PageStatus() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java new file mode 100644 index 00000000..45e614a5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/PageType.java @@ -0,0 +1,49 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 页面类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class PageType { + + /** + * 业务页面。 + */ + public static final int BIZ = 1; + /** + * 统计页面。 + */ + public static final int STATS = 5; + /** + * 流程页面。 + */ + public static final int FLOW = 10; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(BIZ, "业务页面"); + DICT_MAP.put(STATS, "统计页面"); + DICT_MAP.put(FLOW, "流程页面"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private PageType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java new file mode 100644 index 00000000..f14289da --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RelationType.java @@ -0,0 +1,44 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 关联类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class RelationType { + + /** + * 一对一关联。 + */ + public static final int ONE_TO_ONE = 0; + /** + * 一对多关联。 + */ + public static final int ONE_TO_MANY = 1; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(ONE_TO_ONE, "一对一关联"); + DICT_MAP.put(ONE_TO_MANY, "一对多关联"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RelationType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java new file mode 100644 index 00000000..f2b5ee76 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/RuleType.java @@ -0,0 +1,69 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 验证规则类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class RuleType { + + /** + * 只允许整数。 + */ + public static final int INTEGER_ONLY = 1; + /** + * 只允许数字。 + */ + public static final int DIGITAL_ONLY = 2; + /** + * 只允许英文字符。 + */ + public static final int LETTER_ONLY = 3; + /** + * 范围验证。 + */ + public static final int RANGE = 4; + /** + * 邮箱格式验证。 + */ + public static final int EMAIL = 5; + /** + * 手机格式验证。 + */ + public static final int MOBILE = 6; + /** + * 自定义验证。 + */ + public static final int CUSTOM = 100; + + private static final Map DICT_MAP = new HashMap<>(7); + static { + DICT_MAP.put(INTEGER_ONLY, "只允许整数"); + DICT_MAP.put(DIGITAL_ONLY, "只允许数字"); + DICT_MAP.put(LETTER_ONLY, "只允许英文字符"); + DICT_MAP.put(RANGE, "范围验证"); + DICT_MAP.put(EMAIL, "邮箱格式验证"); + DICT_MAP.put(MOBILE, "手机格式验证"); + DICT_MAP.put(CUSTOM, "自定义验证"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RuleType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java new file mode 100644 index 00000000..3d5b9c42 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/model/constant/VirtualType.java @@ -0,0 +1,39 @@ +package com.orangeforms.common.online.model.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 在线表单虚拟字段类型常量字典对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +public final class VirtualType { + + /** + * 聚合。 + */ + public static final int AGGREGATION = 0; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(AGGREGATION, "聚合"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private VirtualType() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java new file mode 100644 index 00000000..8b6291f6 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ColumnData.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.online.object; + +import com.orangeforms.common.online.model.OnlineColumn; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 表字段数据对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ColumnData { + + /** + * 在线表字段对象。 + */ + private OnlineColumn column; + + /** + * 字段值。 + */ + private Object columnValue; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java new file mode 100644 index 00000000..f99e18d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/ConstDictInfo.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.online.object; + +import lombok.Data; + +import java.util.List; + +/** + * 在线表单常量字典的数据结构。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class ConstDictInfo { + + private List dictData; + + @Data + public static class ConstDictData { + private String type; + private Object id; + private String name; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java new file mode 100644 index 00000000..4798b332 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/object/JoinTableInfo.java @@ -0,0 +1,28 @@ +package com.orangeforms.common.online.object; + +import lombok.Data; + +/** + * 连接表信息对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +public class JoinTableInfo { + + /** + * 是否左连接。 + */ + private Boolean leftJoin; + + /** + * 连接表表名。 + */ + private String joinTableName; + + /** + * 连接条件。 + */ + private String joinCondition; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java new file mode 100644 index 00000000..a48a487e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineColumnService.java @@ -0,0 +1,147 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineColumnRule; + +import java.util.List; +import java.util.Set; + +/** + * 字段数据数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineColumnService extends IBaseService { + + /** + * 保存新增数据表字段列表。 + * + * @param columnList 新增数据表字段对象列表。 + * @param onlineTableId 在线表对象的主键Id。 + * @return 插入的在线表字段数据。 + */ + List saveNewList(List columnList, Long onlineTableId); + + /** + * 更新数据对象。 + * + * @param onlineColumn 更新的对象。 + * @param originalOnlineColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineColumn onlineColumn, OnlineColumn originalOnlineColumn); + + /** + * 刷新数据库表字段的数据到在线表字段。 + * + * @param sqlTableColumn 源数据库表字段对象。 + * @param onlineColumn 被刷新的在线表字段对象。 + */ + void refresh(SqlTableColumn sqlTableColumn, OnlineColumn onlineColumn); + + /** + * 删除指定数据。 + * + * @param tableId 表Id。 + * @param columnId 字段Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long tableId, Long columnId); + + /** + * 批量添加多对多关联关系。 + * + * @param onlineColumnRuleList 多对多关联表对象集合。 + * @param columnId 主表Id。 + */ + void addOnlineColumnRuleList(List onlineColumnRuleList, Long columnId); + + /** + * 更新中间表数据。 + * + * @param onlineColumnRule 中间表对象。 + * @return 更新成功与否。 + */ + boolean updateOnlineColumnRule(OnlineColumnRule onlineColumnRule); + + /** + * 获取中间表数据。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 中间表对象。 + */ + OnlineColumnRule getOnlineColumnRule(Long columnId, Long ruleId); + + /** + * 移除单条多对多关系。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 成功返回true,否则false。 + */ + boolean removeOnlineColumnRule(Long columnId, Long ruleId); + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param tableId 主表主键Id。 + * @return 删除数量。 + */ + int removeByTableId(Long tableId); + + /** + * 删除指定数据表Id集合中的表字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + void removeByTableIdSet(Set tableIdSet); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @return 查询结果集。 + */ + List getOnlineColumnList(OnlineColumn filter); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @return 查询结果集。 + */ + List getOnlineColumnListWithRelation(OnlineColumn filter); + + /** + * 获取指定数据表Id集合的字段对象列表。 + * + * @param tableIdSet 指定的数据表Id集合。 + * @return 数据表Id集合所包含的字段对象列表。 + */ + List getOnlineColumnListByTableIds(Set tableIdSet); + + /** + * 根据表Id和字段列名获取指定字段。 + * + * @param tableId 字段所在表Id。 + * @param columnName 字段名。 + * @return 查询出的字段对象。 + */ + OnlineColumn getOnlineColumnByTableIdAndColumnName(Long tableId, String columnName); + + /** + * 验证主键是否正确。 + * + * @param tableColumn 数据库导入的表字段对象。 + * @return 验证结果。 + */ + CallResult verifyPrimaryKey(SqlTableColumn tableColumn); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java new file mode 100644 index 00000000..a96d86b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceRelationService.java @@ -0,0 +1,85 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; + +import java.util.List; +import java.util.Set; + +/** + * 数据关联数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceRelationService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param relation 新增对象。 + * @param slaveSqlTable 新增的关联从数据表对象。 + * @param slaveSqlColumn 新增的关联从数据表对象。 + * @return 返回新增对象。 + */ + OnlineDatasourceRelation saveNew( + OnlineDatasourceRelation relation, SqlTable slaveSqlTable, SqlTableColumn slaveSqlColumn); + + /** + * 更新数据对象。 + * + * @param relation 更新的对象。 + * @param originalRelation 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDatasourceRelation relation, OnlineDatasourceRelation originalRelation); + + /** + * 删除指定数据。 + * + * @param relationId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long relationId); + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param datasourceId 主表主键Id。 + * @return 删除数量。 + */ + int removeByDatasourceId(Long datasourceId); + + /** + * 查询指定数据源Id的数据源关联对象列表。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 在线数据源关联对象列表。 + */ + List getOnlineDatasourceRelationListFromCache(Set datasourceIdSet); + + /** + * 查询指定数据源关联对象。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @return 在线数据源关联对象。 + */ + OnlineDatasourceRelation getOnlineDatasourceRelationFromCache(Long datasourceId, Long relationId); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceRelationList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceRelationListWithRelation( + OnlineDatasourceRelation filter, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java new file mode 100644 index 00000000..f51dddb5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDatasourceService.java @@ -0,0 +1,134 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceTable; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 数据模型数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDatasourceService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineDatasource 新增对象。 + * @param sqlTable 新增的数据表对象。 + * @param pageId 关联的页面Id。 + * @return 返回新增对象。 + */ + OnlineDatasource saveNew(OnlineDatasource onlineDatasource, SqlTable sqlTable, Long pageId); + + /** + * 更新数据对象。 + * + * @param onlineDatasource 更新的对象。 + * @param originalOnlineDatasource 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDatasource onlineDatasource, OnlineDatasource originalOnlineDatasource); + + /** + * 删除指定数据。 + * + * @param datasourceId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long datasourceId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDatasourceListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceList(OnlineDatasource filter, String orderBy); + + /** + * 查询指定数据源Id的数据源对象。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceId 数据源Id。 + * @return 在线数据源对象。 + */ + OnlineDatasource getOnlineDatasourceFromCache(Long datasourceId); + + /** + * 查询指定数据源Id集合的数据源列表。 + * 从缓存中读取,如果不存在会从数据库中读取并同步到Redis中。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 在线数据源对象集合。 + */ + List getOnlineDatasourceListFromCache(Set datasourceIdSet); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceListWithRelation(OnlineDatasource filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param pageId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDatasourceListByPageId(Long pageId, OnlineDatasource filter, String orderBy); + + /** + * 获取指定数据源Id集合所关联的在线表关联数据。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 数据源和数据表的多对多关联列表。 + */ + List getOnlineDatasourceTableList(Set datasourceIdSet); + + /** + * 根据在线表单Id集合,获取关联的在线数据源对象列表。 + * + * @param readFormIdSet 在线表单Id集合。 + * @return 与参数表单Id关联的数据源列表。 + */ + List getOnlineDatasourceListByFormIds(Set readFormIdSet); + + /** + * 根据主表Id获取在线表单数据源对象。 + * + * @param masterTableId 主表Id。 + * @return 在线表单数据源对象。 + */ + OnlineDatasource getOnlineDatasourceByMasterTableId(Long masterTableId); + + /** + * 判断指定数据源变量是否存在。 + * @param variableName 变量名。 + * @return true存在,否则false。 + */ + boolean existByVariableName(String variableName); + + /** + * 获取在线表单页面和在线表单数据源变量名的映射关系。 + * + * @param pageIds 页面Id集合。 + * @return 在线表单页面和在线表单数据源变量名的映射关系。 + */ + Map getPageIdAndVariableNameMapByPageIds(Set pageIds); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java new file mode 100644 index 00000000..d04ace46 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDblinkService.java @@ -0,0 +1,99 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.online.model.OnlineDblink; + +import java.util.List; + +/** + * 数据库链接数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDblinkService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineDblink 新增对象。 + * @return 返回新增对象。 + */ + OnlineDblink saveNew(OnlineDblink onlineDblink); + + /** + * 更新数据对象。 + * + * @param onlineDblink 更新的对象。 + * @param originalOnlineDblink 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDblink onlineDblink, OnlineDblink originalOnlineDblink); + + /** + * 删除指定数据。 + * + * @param dblinkId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long dblinkId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDblinkListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDblinkList(OnlineDblink filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDblinkList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDblinkListWithRelation(OnlineDblink filter, String orderBy); + + /** + * 获取指定DBLink下面的全部数据表。 + * + * @param dblink 数据库链接对象。 + * @return 全部数据表列表。 + */ + List getDblinkTableList(OnlineDblink dblink); + + /** + * 获取指定DBLink下,指定表名的数据表对象,及其关联字段列表。 + * + * @param dblink 数据库链接对象。 + * @param tableName 数据库中的数据表名。 + * @return 数据表对象。 + */ + SqlTable getDblinkTable(OnlineDblink dblink, String tableName); + + /** + * 获取指定DBLink下,指定表名的字段列表。 + * + * @param dblink 数据库链接对象。 + * @param tableName 数据库中的数据表名。 + * @return 表的字段列表。 + */ + List getDblinkTableColumnList(OnlineDblink dblink, String tableName); + + /** + * 获取指定DBLink下,指定表的字段对象。 + * + * @param dblink 数据库链接对象。 + * @param tableName 数据库中的数据表名。 + * @param columnName 数据库中的数据表的字段名。 + * @return 表的字段对象。 + */ + SqlTableColumn getDblinkTableColumn(OnlineDblink dblink, String tableName, String columnName); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java new file mode 100644 index 00000000..4f2c56bd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineDictService.java @@ -0,0 +1,78 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineDict; + +import java.util.List; +import java.util.Set; + +/** + * 在线表单字典数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineDictService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineDict 新增对象。 + * @return 返回新增对象。 + */ + OnlineDict saveNew(OnlineDict onlineDict); + + /** + * 更新数据对象。 + * + * @param onlineDict 更新的对象。 + * @param originalOnlineDict 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineDict onlineDict, OnlineDict originalOnlineDict); + + /** + * 删除指定数据。 + * + * @param dictId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long dictId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDictListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDictList(OnlineDict filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDictList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineDictListWithRelation(OnlineDict filter, String orderBy); + + /** + * 从缓存中获取字典数据。 + * + * @param dictId 字典Id。 + * @return 在线字典对象。 + */ + OnlineDict getOnlineDictFromCache(Long dictId); + + /** + * 从缓存中获取字典数据集合。 + * + * @param dictIdSet 字典Id集合。 + * @return 在线字典对象集合。 + */ + List getOnlineDictListFromCache(Set dictIdSet); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java new file mode 100644 index 00000000..b6334b8d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineFormService.java @@ -0,0 +1,122 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineForm; +import com.orangeforms.common.online.model.OnlineFormDatasource; + +import java.util.List; +import java.util.Set; + +/** + * 在线表单数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineFormService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineForm 新增对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 返回新增对象。 + */ + OnlineForm saveNew(OnlineForm onlineForm, Set datasourceIdSet); + + /** + * 更新数据对象。 + * + * @param onlineForm 更新的对象。 + * @param originalOnlineForm 原有数据对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineForm onlineForm, OnlineForm originalOnlineForm, Set datasourceIdSet); + + /** + * 删除指定数据。 + * + * @param formId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long formId); + + /** + * 根据PageId,删除其所属的所有表单,以及表单关联的数据源数据。 + * + * @param pageId 指定的pageId。 + * @return 删除数量。 + */ + int removeByPageId(Long pageId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineFormListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineFormList(OnlineForm filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineFormList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineFormListWithRelation(OnlineForm filter, String orderBy); + + /** + * 获取使用指定数据表的表单列表。 + * + * @param tableId 数据表Id。 + * @return 使用该数据表的表单列表。 + */ + List getOnlineFormListByTableId(Long tableId); + + /** + * 获取指定表单的数据源列表。 + * 从缓存中读取,如果缓存中不存在,从数据库读取后同步更新到缓存。 + * + * @param formId 指定的表单。 + * @return 表单和数据源的多对多关联对象列表。 + */ + List getFormDatasourceListFromCache(Long formId); + + /** + * 查询正在使用当前数据源的表单。 + * + * @param datasourceId 数据源Id。 + * @return 正在使用当前数据源的表单列表。 + */ + List getOnlineFormListByDatasourceId(Long datasourceId); + + /** + * 查询指定PageId集合的在线表单列表。 + * + * @param pageIdSet 页面Id集合。 + * @return 在线表单集合。 + */ + List getOnlineFormListByPageIds(Set pageIdSet); + + /** + * 从缓存中获取表单数据。 + * + * @param formId 表单Id。 + * @return 在线表单对象。 + */ + OnlineForm getOnlineFormFromCache(Long formId); + + /** + * 判断指定编码的表单是否存在。 + * + * @param formCode 表单编码。 + * @return true存在,否则false。 + */ + boolean existByFormCode(String formCode); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java new file mode 100644 index 00000000..9cde49b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineOperationService.java @@ -0,0 +1,220 @@ +package com.orangeforms.common.online.service; + +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.object.MyPageData; +import com.orangeforms.common.core.object.MyPageParam; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineDict; +import com.orangeforms.common.online.model.OnlineTable; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 在线表单运行时操作的数据服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineOperationService { + + /** + * 待批量插入的所有表数据。 + * + * @param table 在线表对象。 + * @param dataList 数据对象列表。 + */ + void saveNewBatch(OnlineTable table, List dataList); + + /** + * 待插入的所有表数据。 + * + * @param table 在线表对象。 + * @param data 数据对象。 + * @return 主键值。由于自增主键不能获取插入后的主键值,因此返回NULL。 + */ + Object saveNew(OnlineTable table, JSONObject data); + + /** + * 待插入的主表数据和多个从表数据。 + * + * @param masterTable 主表在线表对象。 + * @param masterData 主表数据对象。 + * @param slaveDataListMap 多个从表的数据字段数据。 + * @return 主表的主键值。由于自增主键不能获取插入后的主键值,因此返回NULL。 + */ + Object saveNewWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap); + + /** + * 更新表数据。 + * + * @param table 在线表对象。 + * @param data 单条表数据。 + * @return true 更新成功,否则false。 + */ + boolean update(OnlineTable table, JSONObject data); + + /** + * 更新流程字段的状态。 + * + * @param table 数据表。 + * @param dataId 主键Id。 + * @param column 更新字段。 + * @param dataValue 新的数据值。 + * @return true 更新成功,否则false。 + */ + boolean updateColumn(OnlineTable table, String dataId, OnlineColumn column, T dataValue); + + /** + * 级联更新主表和从表数据。 + * + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param datasourceId 主表数据源Id。 + * @param slaveDataListMap 关联从表数据。 + */ + void updateWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Long datasourceId, + Map> slaveDataListMap); + + /** + * 更新关联从表的数据。 + * + * @param masterTable 主表对象。 + * @param masterData 主表数据。 + * @param masterDataId 主表主键Id。 + * @param datasourceId 主表数据源Id。 + * @param relationId 关联Id。 + * @param slaveDataList 从表数据。 + */ + void updateRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + Long datasourceId, + Long relationId, + List slaveDataList); + + /** + * 删除主表数据,及其需要级联删除的一对多关联从表数据。 + * + * @param table 表对象。 + * @param relationList 一对多关联对象列表。 + * @param dataId 主表主键Id值。 + * @return true 删除成功,否则false。 + */ + boolean delete(OnlineTable table, List relationList, String dataId); + + /** + * 删除一对多从表数据中的关联数据。 + * 删除所有字段为slaveColumn,数据值为columnValue,但是主键值不在keptIdSet中的从表关联数据。 + * + * @param slaveTable 一对多从表。 + * @param slaveColumn 从表关联字段。 + * @param columnValue 关联字段的值。 + * @param keptIdSet 被保留从表数据的主键Id值。 + */ + void deleteOneToManySlaveData( + OnlineTable slaveTable, OnlineColumn slaveColumn, String columnValue, Set keptIdSet); + + /** + * 根据主键判断当前数据是否存在。 + * + * @param table 主表对象。 + * @param dataId 主表主键Id值。 + * @return 存在返回true,否则false。 + */ + boolean existId(OnlineTable table, String dataId); + + /** + * 从数据源和一对一数据源关联中,动态获取数据。 + * + * @param table 主表对象。 + * @param oneToOneRelationList 数据源一对一关联列表。 + * @param allRelationList 数据源全部关联列表。 + * @param dataId 主表主键Id值。 + * @return 查询结果。 + */ + Map getMasterData( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + String dataId); + + /** + * 从一对多数据源关联中,动态获取数据。 + * + * @param relation 一对多数据源关联对象。 + * @param dataId 一对多关联数据主键Id值。 + * @return 查询结果。 + */ + Map getSlaveData(OnlineDatasourceRelation relation, String dataId); + + /** + * 从数据源和一对一数据源关联中,动态获取数据列表。 + * + * @param table 主表对象。 + * @param oneToOneRelationList 数据源一对一关联列表。 + * @param allRelationList 数据源全部关联列表。 + * @param filterList 过滤参数列表。 + * @param orderBy 排序字符串。 + * @param pageParam 分页对象。 + * @return 查询结果集。 + */ + MyPageData> getMasterDataList( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + List filterList, + String orderBy, + MyPageParam pageParam); + + /** + * 从一对多数据源关联中,动态获取数据列表。 + * + * @param relation 一对多数据源关联对象。 + * @param filterList 过滤参数列表。 + * @param orderBy 排序字符串。 + * @param pageParam 分页对象。 + * @return 查询结果集。 + */ + MyPageData> getSlaveDataList( + OnlineDatasourceRelation relation, List filterList, String orderBy, MyPageParam pageParam); + + /** + * 从字典对象指向的数据表中查询数据,并根据参数进行数据过滤。 + * + * @param dict 字典对象。 + * @param filterList 过滤参数列表。 + * @return 查询结果集。 + */ + List> getDictDataList(OnlineDict dict, List filterList); + + /** + * 为主表及其关联表数据绑定字典数据。 + * + * @param masterTable 主表对象。 + * @param relationList 主表依赖的关联列表。 + * @param dataList 数据列表。 + */ + void buildDataListWithDict( + OnlineTable masterTable, List relationList, List> dataList); + + /** + * 获取在线表单所关联的权限数据,包括权限字列表和权限资源列表。 + * + * @param menuFormIds 菜单关联的表单Id集合。 + * @param viewFormIds 查询权限的表单Id集合。 + * @param editFormIds 编辑权限的表单Id集合。 + * @return 在线表单权限数据。 + */ + Map calculatePermData(Set menuFormIds, Set viewFormIds, Set editFormIds); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java new file mode 100644 index 00000000..2ba8458b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlinePageService.java @@ -0,0 +1,138 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlinePage; +import com.orangeforms.common.online.model.OnlinePageDatasource; + +import java.util.List; + +/** + * 在线表单页面数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlinePageService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlinePage 新增对象。 + * @return 返回新增对象。 + */ + OnlinePage saveNew(OnlinePage onlinePage); + + /** + * 更新数据对象。 + * + * @param onlinePage 更新的对象。 + * @param originalOnlinePage 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlinePage onlinePage, OnlinePage originalOnlinePage); + + /** + * 更新页面对象的发布状态。 + * + * @param pageId 页面对象Id。 + * @param published 新的状态。 + */ + void updatePublished(Long pageId, Boolean published); + + /** + * 删除指定数据,及其包含的表单和数据源等。 + * + * @param pageId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long pageId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlinePageListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlinePageList(OnlinePage filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlinePageList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlinePageListWithRelation(OnlinePage filter, String orderBy); + + /** + * 批量添加多对多关联关系。 + * + * @param onlinePageDatasourceList 多对多关联表对象集合。 + * @param pageId 主表Id。 + */ + void addOnlinePageDatasourceList(List onlinePageDatasourceList, Long pageId); + + /** + * 获取中间表数据。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 中间表对象。 + */ + OnlinePageDatasource getOnlinePageDatasource(Long pageId, Long datasourceId); + + /** + * 获取在线页面和数据源中间表数据列表。 + * + * @param pageId 主表Id。 + * @return 在线页面和数据源中间表对象列表。 + */ + List getOnlinePageDatasourceListByPageId(Long pageId); + + /** + * 根据数据源Id,返回使用该数据源的OnlinePage对象。 + * + * @param datasourceId 数据源Id。 + * @return 使用该数据源的页面列表。 + */ + List getOnlinePageListByDatasourceId(Long datasourceId); + + /** + * 移除单条多对多关系。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 成功返回true,否则false。 + */ + boolean removeOnlinePageDatasource(Long pageId, Long datasourceId); + + /** + * 判断指定编码的页面是否存在。 + * + * @param pageCode 页面编码。 + * @return true存在,否则false。 + */ + boolean existByPageCode(String pageCode); + + /** + * 查询主键Id集合中不存在的,且租户Id为NULL的在线表单页面列表。 + * + * @param pageIds 主键Id集合。 + * @param orderBy 排序字符串。 + * @return 在线表单页面列表。 + */ + List getNotInListWithNonTenant(List pageIds, String orderBy); + + /** + * 查询主键Id集合中存在的,且租户Id为NULL的在线表单页面列表。 + * + * @param pageIds 主键Id集合。 + * @param orderBy 排序字符串。 + * @return 在线表单页面列表。 + */ + List getInListWithNonTenant(List pageIds, String orderBy); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java new file mode 100644 index 00000000..f381a43d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineRuleService.java @@ -0,0 +1,91 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineColumnRule; +import com.orangeforms.common.online.model.OnlineRule; + +import java.util.List; +import java.util.Set; + +/** + * 验证规则数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineRuleService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineRule 新增对象。 + * @return 返回新增对象。 + */ + OnlineRule saveNew(OnlineRule onlineRule); + + /** + * 更新数据对象。 + * + * @param onlineRule 更新的对象。 + * @param originalOnlineRule 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineRule onlineRule, OnlineRule originalOnlineRule); + + /** + * 删除指定数据。 + * + * @param ruleId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long ruleId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineRuleListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineRuleList(OnlineRule filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineRuleList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineRuleListWithRelation(OnlineRule filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getNotInOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy); + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy); + + /** + * 返回指定字段Id列表关联的字段规则对象列表。 + * + * @param columnIdSet 指定的字段Id列表。 + * @return 关联的字段规则对象列表。 + */ + List getOnlineColumnRuleListByColumnIds(Set columnIdSet); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java new file mode 100644 index 00000000..e30f7fba --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineTableService.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineTable; + +import java.util.List; +import java.util.Set; + +/** + * 数据表数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineTableService extends IBaseService { + + /** + * 基于数据库表保存新增对象。 + * + * @param sqlTable 数据库表对象。 + * @return 返回新增对象。 + */ + OnlineTable saveNewFromSqlTable(SqlTable sqlTable); + + /** + * 删除指定表及其关联的字段数据。 + * + * @param tableId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long tableId); + + /** + * 删除指定数据表Id集合中的表,及其关联字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + void removeByTableIdSet(Set tableIdSet); + + /** + * 根据数据源Id,获取该数据源及其关联所引用的数据表列表。 + * + * @param datasourceId 指定的数据源Id。 + * @return 该数据源及其关联所引用的数据表列表。 + */ + List getOnlineTableListByDatasourceId(Long datasourceId); + + /** + * 从缓存中获取指定的表数据及其关联字段列表。优先从缓存中读取,如果不存在则从数据库中读取,并同步到缓存。 + * 该接口方法仅仅用户在线表单的动态数据操作接口,而非在线表单的配置接口。 + * + * @param tableId 表主键Id。 + * @return 查询后的在线表对象。 + */ + OnlineTable getOnlineTableFromCache(Long tableId); + + /** + * 从缓存中获取指定的表字段。优先从缓存中读取,如果不存在则从数据库中读取,并同步到缓存。 + * 该接口方法仅仅用户在线表单的动态数据操作接口,而非在线表单的配置接口。 + * + * @param tableId 表主键Id。 + * @param columnId 字段Id。 + * @return 查询后的在线表对象。 + */ + OnlineColumn getOnlineColumnFromCache(Long tableId, Long columnId); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java new file mode 100644 index 00000000..710c3a51 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/OnlineVirtualColumnService.java @@ -0,0 +1,68 @@ +package com.orangeforms.common.online.service; + +import com.orangeforms.common.core.base.service.IBaseService; +import com.orangeforms.common.online.model.OnlineVirtualColumn; + +import java.util.*; + +/** + * 虚拟字段数据操作服务接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface OnlineVirtualColumnService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param onlineVirtualColumn 新增对象。 + * @return 返回新增对象。 + */ + OnlineVirtualColumn saveNew(OnlineVirtualColumn onlineVirtualColumn); + + /** + * 更新数据对象。 + * + * @param onlineVirtualColumn 更新的对象。 + * @param originalOnlineVirtualColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(OnlineVirtualColumn onlineVirtualColumn, OnlineVirtualColumn originalOnlineVirtualColumn); + + /** + * 删除指定数据。 + * + * @param virtualColumnId 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long virtualColumnId); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineVirtualColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineVirtualColumnList(OnlineVirtualColumn filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineVirtualColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getOnlineVirtualColumnListWithRelation(OnlineVirtualColumn filter, String orderBy); + + /** + * 根据数据表的集合,查询关联的虚拟字段数据列表。 + * @param tableIdSet 在线数据表Id集合。 + * @return 关联的虚拟字段数据列表。 + */ + List getOnlineVirtualColumnListByTableIds(Set tableIdSet); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java new file mode 100644 index 00000000..4e765927 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineColumnServiceImpl.java @@ -0,0 +1,365 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.util.OnlineDataSourceUtil; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.orangeforms.common.online.dao.OnlineColumnMapper; +import com.orangeforms.common.online.dao.OnlineColumnRuleMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineColumnRule; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.github.pagehelper.Page; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * 字段数据数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineColumnService") +public class OnlineColumnServiceImpl extends BaseService implements OnlineColumnService { + + @Autowired + private OnlineColumnMapper onlineColumnMapper; + @Autowired + private OnlineColumnRuleMapper onlineColumnRuleMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineColumnMapper; + } + + /** + * 保存新增数据表字段列表。 + * + * @param columnList 新增数据表字段对象列表。 + * @param onlineTableId 在线表对象的主键Id。 + * @return 插入的在线表字段数据。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public List saveNewList(List columnList, Long onlineTableId) { + List onlineColumnList = new LinkedList<>(); + if (CollUtil.isEmpty(columnList)) { + return onlineColumnList; + } + this.evictTableCache(onlineTableId); + for (SqlTableColumn column : columnList) { + OnlineColumn onlineColumn = new OnlineColumn(); + BeanUtil.copyProperties(column, onlineColumn, false); + onlineColumn.setColumnId(idGenerator.nextLongId()); + onlineColumn.setTableId(onlineTableId); + this.setDefault(column, onlineColumn); + onlineColumnMapper.insert(onlineColumn); + onlineColumnList.add(onlineColumn); + } + return onlineColumnList; + } + + /** + * 更新数据对象。 + * + * @param onlineColumn 更新的对象。 + * @param originalOnlineColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineColumn onlineColumn, OnlineColumn originalOnlineColumn) { + this.evictTableCache(onlineColumn.getTableId()); + onlineColumn.setUpdateTime(new Date()); + onlineColumn.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + onlineColumn.setCreateTime(originalOnlineColumn.getCreateTime()); + onlineColumn.setCreateUserId(originalOnlineColumn.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(onlineColumn, onlineColumn.getColumnId()); + return onlineColumnMapper.update(onlineColumn, uw) == 1; + } + + /** + * 刷新数据库表字段的数据到在线表字段。 + * + * @param sqlTableColumn 源数据库表字段对象。 + * @param onlineColumn 被刷新的在线表字段对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void refresh(SqlTableColumn sqlTableColumn, OnlineColumn onlineColumn) { + this.evictTableCache(onlineColumn.getTableId()); + BeanUtil.copyProperties(sqlTableColumn, onlineColumn, false); + String objectFieldName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, onlineColumn.getColumnName()); + onlineColumn.setObjectFieldName(objectFieldName); + String objectFieldType = convertToJavaType(onlineColumn, sqlTableColumn.getDblinkType()); + onlineColumn.setObjectFieldType(objectFieldType); + onlineColumnMapper.updateById(onlineColumn); + } + + /** + * 删除指定数据。 + * + * @param tableId 表Id。 + * @param columnId 字段Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long tableId, Long columnId) { + this.evictTableCache(tableId); + return onlineColumnMapper.deleteById(columnId) == 1; + } + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param tableId 主表主键Id。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int removeByTableId(Long tableId) { + OnlineColumn deletedObject = new OnlineColumn(); + deletedObject.setTableId(tableId); + return onlineColumnMapper.delete(new QueryWrapper<>(deletedObject)); + } + + /** + * 删除指定数据表Id集合中的表字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByTableIdSet(Set tableIdSet) { + onlineColumnMapper.delete(new QueryWrapper().lambda().in(OnlineColumn::getTableId, tableIdSet)); + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @return 查询结果集。 + */ + @Override + public List getOnlineColumnList(OnlineColumn filter) { + return onlineColumnMapper.getOnlineColumnList(filter); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @return 查询结果集。 + */ + @Override + public List getOnlineColumnListWithRelation(OnlineColumn filter) { + List resultList = onlineColumnMapper.getOnlineColumnList(filter); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 获取指定数据表Id集合的字段对象列表。 + * + * @param tableIdSet 指定的数据表Id集合。 + * @return 数据表Id集合所包含的字段对象列表。 + */ + @Override + public List getOnlineColumnListByTableIds(Set tableIdSet) { + return onlineColumnMapper.selectList( + new QueryWrapper().lambda().in(OnlineColumn::getTableId, tableIdSet)); + } + + /** + * 根据表Id和字段列名获取指定字段。 + * + * @param tableId 字段所在表Id。 + * @param columnName 字段名。 + * @return 查询出的字段对象。 + */ + @Override + public OnlineColumn getOnlineColumnByTableIdAndColumnName(Long tableId, String columnName) { + OnlineColumn filter = new OnlineColumn(); + filter.setTableId(tableId); + filter.setColumnName(columnName); + return onlineColumnMapper.selectOne(new QueryWrapper<>(filter)); + } + + @Override + public CallResult verifyPrimaryKey(SqlTableColumn tableColumn) { + Assert.isTrue(tableColumn.getPrimaryKey()); + OnlineColumn onlineColumn = new OnlineColumn(); + BeanUtil.copyProperties(tableColumn, onlineColumn, false); + String javaType = this.convertToJavaType(onlineColumn, tableColumn.getDblinkType()); + if (ObjectFieldType.INTEGER.equals(javaType)) { + if (BooleanUtil.isFalse(onlineColumn.getAutoIncrement())) { + return CallResult.error("字段验证失败,整型主键必须是自增主键!"); + } + } else { + if (!StrUtil.equalsAny(javaType, ObjectFieldType.LONG, ObjectFieldType.STRING)) { + return CallResult.error("字段验证失败,不合法的主键类型 [" + tableColumn.getColumnType() + "]!"); + } + } + return CallResult.ok(); + } + + /** + * 批量添加多对多关联关系。 + * + * @param onlineColumnRuleList 多对多关联表对象集合。 + * @param columnId 主表Id。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void addOnlineColumnRuleList(List onlineColumnRuleList, Long columnId) { + this.evictTableCacheByColumnId(columnId); + for (OnlineColumnRule onlineColumnRule : onlineColumnRuleList) { + onlineColumnRule.setColumnId(columnId); + onlineColumnRuleMapper.insert(onlineColumnRule); + } + } + + /** + * 更新中间表数据。 + * + * @param onlineColumnRule 中间表对象。 + * @return 更新成功与否。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateOnlineColumnRule(OnlineColumnRule onlineColumnRule) { + this.evictTableCacheByColumnId(onlineColumnRule.getColumnId()); + OnlineColumnRule filter = new OnlineColumnRule(); + filter.setColumnId(onlineColumnRule.getColumnId()); + filter.setRuleId(onlineColumnRule.getRuleId()); + UpdateWrapper uw = + BaseService.createUpdateQueryForNullValue(onlineColumnRule, OnlineColumnRule.class); + uw.setEntity(filter); + return onlineColumnRuleMapper.update(onlineColumnRule, uw) > 0; + } + + /** + * 获取中间表数据。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 中间表对象。 + */ + @Override + public OnlineColumnRule getOnlineColumnRule(Long columnId, Long ruleId) { + OnlineColumnRule filter = new OnlineColumnRule(); + filter.setColumnId(columnId); + filter.setRuleId(ruleId); + return onlineColumnRuleMapper.selectOne(new QueryWrapper<>(filter)); + } + + /** + * 移除单条多对多关系。 + * + * @param columnId 主表Id。 + * @param ruleId 从表Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeOnlineColumnRule(Long columnId, Long ruleId) { + this.evictTableCacheByColumnId(columnId); + OnlineColumnRule filter = new OnlineColumnRule(); + filter.setColumnId(columnId); + filter.setRuleId(ruleId); + return onlineColumnRuleMapper.delete(new QueryWrapper<>(filter)) > 0; + } + + private void setDefault(SqlTableColumn column, OnlineColumn onlineColumn) { + String objectFieldName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, onlineColumn.getColumnName()); + onlineColumn.setObjectFieldName(objectFieldName); + String objectFieldType = convertToJavaType(onlineColumn, column.getDblinkType()); + onlineColumn.setObjectFieldType(objectFieldType); + onlineColumn.setFilterType(FieldFilterType.NO_FILTER); + onlineColumn.setParentKey(false); + onlineColumn.setDeptFilter(false); + onlineColumn.setUserFilter(false); + if (onlineColumn.getAutoIncrement() == null) { + onlineColumn.setAutoIncrement(false); + } + onlineColumn.setUploadFileSystemType(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal()); + Date now = new Date(); + onlineColumn.setUpdateTime(now); + onlineColumn.setCreateTime(now); + onlineColumn.setCreateUserId(TokenData.takeFromRequest().getUserId()); + onlineColumn.setUpdateUserId(onlineColumn.getCreateUserId()); + } + + private void evictTableCache(Long tableId) { + String tableIdKey = OnlineRedisKeyUtil.makeOnlineTableKey(tableId); + redissonClient.getBucket(tableIdKey).delete(); + } + + private void evictTableCacheByColumnId(Long columnId) { + OnlineColumn column = this.getById(columnId); + if (column != null) { + this.evictTableCache(column.getTableId()); + } + } + + private String convertToJavaType(OnlineColumn column, int dblinkType) { + DataSourceProvider provider = dataSourceUtil.getProvider(dblinkType); + if (provider == null) { + throw new MyRuntimeException("Unsupported Data Type"); + } + return provider.convertColumnTypeToJavaType( + column.getColumnType(), column.getNumericPrecision(), column.getNumericScale()); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java new file mode 100644 index 00000000..4cb53ee5 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceRelationServiceImpl.java @@ -0,0 +1,289 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.orangeforms.common.online.dao.OnlineDatasourceRelationMapper; +import com.orangeforms.common.online.dao.OnlineDatasourceTableMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineDatasourceTable; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.github.pagehelper.Page; +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.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 数据源关联数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDatasourceRelationService") +public class OnlineDatasourceRelationServiceImpl + extends BaseService implements OnlineDatasourceRelationService { + + @Autowired + private OnlineDatasourceRelationMapper onlineDatasourceRelationMapper; + @Autowired + private OnlineDatasourceTableMapper onlineDatasourceTableMapper; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + @Autowired + private CommonRedisUtil commonRedisUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDatasourceRelationMapper; + } + + /** + * 保存新增对象。 + * + * @param relation 新增对象。 + * @param slaveSqlTable 新增的关联从数据表对象。 + * @param slaveSqlColumn 新增的关联从数据表对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDatasourceRelation saveNew( + OnlineDatasourceRelation relation, SqlTable slaveSqlTable, SqlTableColumn slaveSqlColumn) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(relation.getDatasourceId())); + // 查找数据源关联的数据表,判断当前关联的从表,是否已经存在于zz_online_datasource_table中了。 + // 对于同一个数据源及其关联,同一个数据表只会被创建一次,如果已经和当前数据源的其他Relation, + // 作为从表绑定了,怎么就可以直接使用这个OnlineTable了,否则就会为这个SqlTable,创建对应的OnlineTable。 + List datasourceTableList = + onlineTableService.getOnlineTableListByDatasourceId(relation.getDatasourceId()); + OnlineTable relationSlaveTable = null; + OnlineColumn relationSlaveColumn = null; + for (OnlineTable onlineTable : datasourceTableList) { + if (onlineTable.getTableName().equals(slaveSqlTable.getTableName())) { + relationSlaveTable = onlineTable; + relationSlaveColumn = onlineColumnService.getOnlineColumnByTableIdAndColumnName( + onlineTable.getTableId(), slaveSqlColumn.getColumnName()); + break; + } + } + if (relationSlaveTable == null) { + relationSlaveTable = onlineTableService.saveNewFromSqlTable(slaveSqlTable); + for (OnlineColumn onlineColumn : relationSlaveTable.getColumnList()) { + if (onlineColumn.getColumnName().equals(slaveSqlColumn.getColumnName())) { + relationSlaveColumn = onlineColumn; + break; + } + } + } + TokenData tokenData = TokenData.takeFromRequest(); + relation.setRelationId(idGenerator.nextLongId()); + relation.setAppCode(tokenData.getAppCode()); + relation.setSlaveTableId(relationSlaveTable.getTableId()); + relation.setSlaveColumnId(relationSlaveColumn == null ? null : relationSlaveColumn.getColumnId()); + Date now = new Date(); + relation.setUpdateTime(now); + relation.setCreateTime(now); + relation.setCreateUserId(tokenData.getUserId()); + relation.setUpdateUserId(tokenData.getUserId()); + onlineDatasourceRelationMapper.insert(relation); + OnlineDatasourceTable datasourceTable = new OnlineDatasourceTable(); + datasourceTable.setId(idGenerator.nextLongId()); + datasourceTable.setDatasourceId(relation.getDatasourceId()); + datasourceTable.setRelationId(relation.getRelationId()); + datasourceTable.setTableId(relation.getSlaveTableId()); + onlineDatasourceTableMapper.insert(datasourceTable); + return relation; + } + + /** + * 更新数据对象。 + * + * @param relation 更新的对象。 + * @param originalRelation 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDatasourceRelation relation, OnlineDatasourceRelation originalRelation) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(relation.getDatasourceId())); + TokenData tokenData = TokenData.takeFromRequest(); + relation.setAppCode(tokenData.getAppCode()); + relation.setUpdateTime(new Date()); + relation.setUpdateUserId(tokenData.getUserId()); + relation.setCreateTime(originalRelation.getCreateTime()); + relation.setCreateUserId(originalRelation.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = + this.createUpdateQueryForNullValue(relation, relation.getRelationId()); + return onlineDatasourceRelationMapper.update(relation, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param relationId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long relationId) { + OnlineDatasourceRelation relation = this.getById(relationId); + if (relation != null) { + commonRedisUtil.evictFormCache( + OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(relation.getDatasourceId())); + } + if (onlineDatasourceRelationMapper.deleteById(relationId) != 1) { + return false; + } + OnlineDatasourceTable filter = new OnlineDatasourceTable(); + filter.setRelationId(relationId); + QueryWrapper queryWrapper = new QueryWrapper<>(filter); + OnlineDatasourceTable datasourceTable = onlineDatasourceTableMapper.selectOne(queryWrapper); + onlineDatasourceTableMapper.delete(queryWrapper); + filter = new OnlineDatasourceTable(); + filter.setDatasourceId(datasourceTable.getDatasourceId()); + filter.setTableId(datasourceTable.getTableId()); + // 不在有引用该表的时候,可以删除该数据源关联引用的从表了。 + if (onlineDatasourceTableMapper.selectCount(new QueryWrapper<>(filter)) == 0) { + onlineTableService.remove(datasourceTable.getTableId()); + } + return true; + } + + /** + * 当前服务的支持表为从表,根据主表的主键Id,删除一对多的从表数据。 + * + * @param datasourceId 主表主键Id。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int removeByDatasourceId(Long datasourceId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(datasourceId)); + OnlineDatasourceRelation deletedObject = new OnlineDatasourceRelation(); + deletedObject.setDatasourceId(datasourceId); + return onlineDatasourceRelationMapper.delete(new QueryWrapper<>(deletedObject)); + } + + @Override + public List getOnlineDatasourceRelationListFromCache(Set datasourceIdSet) { + List resultList = new LinkedList<>(); + datasourceIdSet.forEach(datasourceId -> { + String key = OnlineRedisKeyUtil.makeOnlineDataSourceRelationKey(datasourceId); + RBucket bucket = redissonClient.getBucket(key); + if (bucket.isExists()) { + resultList.addAll(JSONArray.parseArray(bucket.get(), OnlineDatasourceRelation.class)); + } else { + OnlineDatasourceRelation filter = new OnlineDatasourceRelation(); + filter.setDatasourceId(datasourceId); + List relationList = this.getListByFilter(filter); + if (CollUtil.isNotEmpty(relationList)) { + resultList.addAll(relationList); + bucket.set(JSONArray.toJSONString(relationList)); + } + } + }); + return resultList; + } + + @Override + public OnlineDatasourceRelation getOnlineDatasourceRelationFromCache(Long datasourceId, Long relationId) { + List relationList = + this.getOnlineDatasourceRelationListFromCache(CollUtil.newHashSet(datasourceId)); + if (CollUtil.isEmpty(relationList)) { + return null; + } + return relationList.stream().filter(r -> r.getRelationId().equals(relationId)).findFirst().orElse(null); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceRelationList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceRelationListWithRelation( + OnlineDatasourceRelation filter, String orderBy) { + if (filter == null) { + filter = new OnlineDatasourceRelation(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + List resultList = + onlineDatasourceRelationMapper.getOnlineDatasourceRelationList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param relation 最新数据对象。 + * @param originalRelation 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData( + OnlineDatasourceRelation relation, OnlineDatasourceRelation originalRelation) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + if (this.needToVerify(relation, originalRelation, OnlineDatasourceRelation::getMasterColumnId) + && !onlineColumnService.existId(relation.getMasterColumnId())) { + return CallResult.error(String.format(errorMessageFormat, "主表关联字段Id")); + } + if (this.needToVerify(relation, originalRelation, OnlineDatasourceRelation::getSlaveTableId) + && !onlineTableService.existId(relation.getSlaveTableId())) { + return CallResult.error(String.format(errorMessageFormat, "从表Id")); + } + if (this.needToVerify(relation, originalRelation, OnlineDatasourceRelation::getSlaveColumnId) + && !onlineColumnService.existId(relation.getSlaveColumnId())) { + return CallResult.error(String.format(errorMessageFormat, "从表关联字段Id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java new file mode 100644 index 00000000..0efb7d86 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDatasourceServiceImpl.java @@ -0,0 +1,270 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineDatasourceMapper; +import com.orangeforms.common.online.dao.OnlineDatasourceTableMapper; +import com.orangeforms.common.online.dao.OnlinePageDatasourceMapper; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceTable; +import com.orangeforms.common.online.model.OnlinePageDatasource; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.github.pagehelper.Page; +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.*; +import java.util.stream.Collectors; + +/** + * 数据模型数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDatasourceService") +public class OnlineDatasourceServiceImpl extends BaseService implements OnlineDatasourceService { + + @Autowired + private OnlineDatasourceMapper onlineDatasourceMapper; + @Autowired + private OnlinePageDatasourceMapper onlinePageDatasourceMapper; + @Autowired + private OnlineDatasourceTableMapper onlineDatasourceTableMapper; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDatasourceMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineDatasource 新增对象。 + * @param sqlTable 新增的数据表对象。 + * @param pageId 关联的页面Id。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDatasource saveNew(OnlineDatasource onlineDatasource, SqlTable sqlTable, Long pageId) { + TokenData tokenData = TokenData.takeFromRequest(); + OnlineTable onlineTable = onlineTableService.saveNewFromSqlTable(sqlTable); + onlineDatasource.setDatasourceId(idGenerator.nextLongId()); + onlineDatasource.setAppCode(tokenData.getAppCode()); + onlineDatasource.setMasterTableId(onlineTable.getTableId()); + Date now = new Date(); + onlineDatasource.setUpdateTime(now); + onlineDatasource.setCreateTime(now); + onlineDatasource.setCreateUserId(tokenData.getUserId()); + onlineDatasource.setUpdateUserId(tokenData.getUserId()); + onlineDatasourceMapper.insert(onlineDatasource); + OnlineDatasourceTable datasourceTable = new OnlineDatasourceTable(); + datasourceTable.setId(idGenerator.nextLongId()); + datasourceTable.setDatasourceId(onlineDatasource.getDatasourceId()); + datasourceTable.setTableId(onlineDatasource.getMasterTableId()); + onlineDatasourceTableMapper.insert(datasourceTable); + OnlinePageDatasource onlinePageDatasource = new OnlinePageDatasource(); + onlinePageDatasource.setId(idGenerator.nextLongId()); + onlinePageDatasource.setPageId(pageId); + onlinePageDatasource.setDatasourceId(onlineDatasource.getDatasourceId()); + onlinePageDatasourceMapper.insert(onlinePageDatasource); + return onlineDatasource; + } + + /** + * 更新数据对象。 + * + * @param onlineDatasource 更新的对象。 + * @param originalOnlineDatasource 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDatasource onlineDatasource, OnlineDatasource originalOnlineDatasource) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceKey(onlineDatasource.getDatasourceId())); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDatasource.setAppCode(tokenData.getAppCode()); + onlineDatasource.setUpdateTime(new Date()); + onlineDatasource.setUpdateUserId(tokenData.getUserId()); + onlineDatasource.setCreateTime(originalOnlineDatasource.getCreateTime()); + onlineDatasource.setCreateUserId(originalOnlineDatasource.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = + this.createUpdateQueryForNullValue(onlineDatasource, onlineDatasource.getDatasourceId()); + return onlineDatasourceMapper.update(onlineDatasource, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param datasourceId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long datasourceId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDataSourceKey(datasourceId)); + if (onlineDatasourceMapper.deleteById(datasourceId) == 0) { + return false; + } + onlineDatasourceRelationService.removeByDatasourceId(datasourceId); + // 开始删除多对多父表的关联 + OnlinePageDatasource onlinePageDatasource = new OnlinePageDatasource(); + onlinePageDatasource.setDatasourceId(datasourceId); + onlinePageDatasourceMapper.delete(new QueryWrapper<>(onlinePageDatasource)); + OnlineDatasourceTable filter = new OnlineDatasourceTable(); + filter.setDatasourceId(datasourceId); + QueryWrapper queryWrapper = new QueryWrapper<>(filter); + List datasourceTableList = onlineDatasourceTableMapper.selectList(queryWrapper); + onlineDatasourceTableMapper.delete(queryWrapper); + Set tableIdSet = datasourceTableList.stream() + .map(OnlineDatasourceTable::getTableId).collect(Collectors.toSet()); + onlineTableService.removeByTableIdSet(tableIdSet); + return true; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDatasourceListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceList(OnlineDatasource filter, String orderBy) { + if (filter == null) { + filter = new OnlineDatasource(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineDatasourceMapper.getOnlineDatasourceList(filter, orderBy); + } + + @Override + public OnlineDatasource getOnlineDatasourceFromCache(Long datasourceId) { + String key = OnlineRedisKeyUtil.makeOnlineDataSourceKey(datasourceId); + return commonRedisUtil.getFromCache(key, datasourceId, this::getById, OnlineDatasource.class); + } + + @Override + public List getOnlineDatasourceListFromCache(Set datasourceIdSet) { + List resultList = new LinkedList<>(); + datasourceIdSet.forEach(datasourceId -> resultList.add(this.getOnlineDatasourceFromCache(datasourceId))); + return resultList; + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDatasourceList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceListWithRelation(OnlineDatasource filter, String orderBy) { + List resultList = this.getOnlineDatasourceList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param pageId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDatasourceListByPageId(Long pageId, OnlineDatasource filter, String orderBy) { + List resultList = + onlineDatasourceMapper.getOnlineDatasourceListByPageId(pageId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + /** + * 获取指定数据源Id集合所关联的在线表关联数据。 + * + * @param datasourceIdSet 数据源Id集合。 + * @return 数据源和数据表的多对多关联列表。 + */ + @Override + public List getOnlineDatasourceTableList(Set datasourceIdSet) { + return onlineDatasourceTableMapper.selectList(new QueryWrapper() + .lambda().in(OnlineDatasourceTable::getDatasourceId, datasourceIdSet)); + } + + /** + * 根据在线表单Id集合,获取关联的在线数据源对象列表。 + * + * @param formIdSet 在线表单Id集合。 + * @return 与参数表单Id关联的数据源列表。 + */ + @Override + public List getOnlineDatasourceListByFormIds(Set formIdSet) { + return onlineDatasourceMapper.getOnlineDatasourceListByFormIds(formIdSet); + } + + @Override + public OnlineDatasource getOnlineDatasourceByMasterTableId(Long masterTableId) { + return onlineDatasourceMapper.selectOne( + new LambdaQueryWrapper().eq(OnlineDatasource::getMasterTableId, masterTableId)); + } + + @Override + public boolean existByVariableName(String variableName) { + OnlineDatasource filter = new OnlineDatasource(); + filter.setVariableName(variableName); + return CollUtil.isNotEmpty(this.getOnlineDatasourceList(filter, null)); + } + + @Override + public Map getPageIdAndVariableNameMapByPageIds(Set pageIds) { + String ids = CollUtil.join(pageIds, ","); + List> dataList = onlineDatasourceMapper.getPageIdAndVariableNameMapByPageIds(ids); + return dataList.stream() + .collect(Collectors.toMap(c -> (Long) c.get("page_id"), c -> (String) c.get("variable_name"))); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java new file mode 100644 index 00000000..24198e62 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDblinkServiceImpl.java @@ -0,0 +1,203 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.constant.DblinkType; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.dbutil.object.SqlTableColumn; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dao.OnlineDblinkMapper; +import com.orangeforms.common.online.model.OnlineDblink; +import com.orangeforms.common.online.service.OnlineDblinkService; +import com.orangeforms.common.online.util.OnlineDataSourceUtil; +import com.github.pagehelper.Page; +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.*; +import java.util.stream.Collectors; + +/** + * 数据库链接数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDblinkService") +public class OnlineDblinkServiceImpl extends BaseService implements OnlineDblinkService { + + @Autowired + private OnlineDblinkMapper onlineDblinkMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDblinkMapper; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDblink saveNew(OnlineDblink onlineDblink) { + onlineDblinkMapper.insert(this.buildDefaultValue(onlineDblink)); + return onlineDblink; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDblink onlineDblink, OnlineDblink originalOnlineDblink) { + if (!StrUtil.equals(onlineDblink.getConfiguration(), originalOnlineDblink.getConfiguration())) { + dataSourceUtil.removeDataSource(onlineDblink.getDblinkId()); + } + onlineDblink.setAppCode(TokenData.takeFromRequest().getAppCode()); + onlineDblink.setCreateUserId(originalOnlineDblink.getCreateUserId()); + onlineDblink.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + onlineDblink.setCreateTime(originalOnlineDblink.getCreateTime()); + onlineDblink.setUpdateTime(new Date()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(onlineDblink, onlineDblink.getDblinkId()); + return onlineDblinkMapper.update(onlineDblink, uw) == 1; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dblinkId) { + dataSourceUtil.removeDataSource(dblinkId); + return onlineDblinkMapper.deleteById(dblinkId) == 1; + } + + @Override + public List getOnlineDblinkList(OnlineDblink filter, String orderBy) { + if (filter == null) { + filter = new OnlineDblink(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineDblinkMapper.getOnlineDblinkList(filter, orderBy); + } + + @Override + public List getOnlineDblinkListWithRelation(OnlineDblink filter, String orderBy) { + List resultList = this.getOnlineDblinkList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public List getDblinkTableList(OnlineDblink dblink) { + List resultList = dataSourceUtil.getTableList(dblink.getDblinkId(), null); + if (StrUtil.isNotBlank(onlineProperties.getTablePrefix())) { + resultList = resultList.stream() + .filter(t -> StrUtil.startWith(t.getTableName(), onlineProperties.getTablePrefix())) + .collect(Collectors.toList()); + } + resultList.forEach(t -> t.setDblinkId(dblink.getDblinkId())); + return resultList; + } + + @Override + public SqlTable getDblinkTable(OnlineDblink dblink, String tableName) { + SqlTable sqlTable = dataSourceUtil.getTable(dblink.getDblinkId(), tableName); + sqlTable.setDblinkId(dblink.getDblinkId()); + sqlTable.setColumnList(getDblinkTableColumnList(dblink, tableName)); + return sqlTable; + } + + @Override + public List getDblinkTableColumnList(OnlineDblink dblink, String tableName) { + List columnList = dataSourceUtil.getTableColumnList(dblink.getDblinkId(), tableName); + columnList.forEach(c -> this.makeupSqlTableColumn(c, dblink.getDblinkType())); + return columnList; + } + + @Override + public SqlTableColumn getDblinkTableColumn(OnlineDblink dblink, String tableName, String columnName) { + List columnList = dataSourceUtil.getTableColumnList(dblink.getDblinkId(), tableName); + SqlTableColumn sqlTableColumn = columnList.stream() + .filter(c -> c.getColumnName().equals(columnName)).findFirst().orElse(null); + if (sqlTableColumn != null) { + this.makeupSqlTableColumn(sqlTableColumn, dblink.getDblinkType()); + } + return sqlTableColumn; + } + + private void makeupSqlTableColumn(SqlTableColumn sqlTableColumn, int dblinkType) { + sqlTableColumn.setDblinkType(dblinkType); + switch (dblinkType) { + case DblinkType.POSTGRESQL: + case DblinkType.OPENGAUSS: + if (StrUtil.equalsAny(sqlTableColumn.getColumnType(), "char", "varchar")) { + sqlTableColumn.setFullColumnType( + sqlTableColumn.getColumnType() + "(" + sqlTableColumn.getStringPrecision() + ")"); + } else { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType()); + } + break; + case DblinkType.MYSQL: + sqlTableColumn.setAutoIncrement("auto_increment".equals(sqlTableColumn.getExtra())); + break; + case DblinkType.ORACLE: + if (StrUtil.equalsAny(sqlTableColumn.getColumnType(), "VARCHAR2", "NVARCHAR2", "CHAR", "NCHAR")) { + sqlTableColumn.setFullColumnType( + sqlTableColumn.getColumnType() + "(" + sqlTableColumn.getStringPrecision() + ")"); + } else if (StrUtil.equals(sqlTableColumn.getColumnType(), "NUMBER")) { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType() + + "(" + sqlTableColumn.getNumericPrecision() + "," + sqlTableColumn.getNumericScale() + ")"); + } else { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType()); + } + break; + case DblinkType.DAMENG: + case DblinkType.KINGBASE: + if (StrUtil.equalsAnyIgnoreCase(sqlTableColumn.getColumnType(), "VARCHAR", "VARCHAR2", "CHAR")) { + sqlTableColumn.setFullColumnType( + sqlTableColumn.getColumnType() + "(" + sqlTableColumn.getStringPrecision() + ")"); + } else if (StrUtil.equals(sqlTableColumn.getColumnType(), "NUMBER")) { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType() + + "(" + sqlTableColumn.getNumericPrecision() + "," + sqlTableColumn.getNumericScale() + ")"); + } else { + sqlTableColumn.setFullColumnType(sqlTableColumn.getColumnType()); + } + break; + default: + break; + } + } + + private OnlineDblink buildDefaultValue(OnlineDblink onlineDblink) { + onlineDblink.setDblinkId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDblink.setCreateUserId(tokenData.getUserId()); + onlineDblink.setUpdateUserId(tokenData.getUserId()); + Date now = new Date(); + onlineDblink.setCreateTime(now); + onlineDblink.setUpdateTime(now); + onlineDblink.setAppCode(tokenData.getAppCode()); + return onlineDblink; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java new file mode 100644 index 00000000..0eca2dc1 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineDictServiceImpl.java @@ -0,0 +1,189 @@ +package com.orangeforms.common.online.service.impl; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.orangeforms.common.online.dao.OnlineDictMapper; +import com.orangeforms.common.online.model.OnlineDict; +import com.orangeforms.common.online.service.OnlineDblinkService; +import com.orangeforms.common.online.service.OnlineDictService; +import com.github.pagehelper.Page; +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.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * 在线表单字典数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineDictService") +public class OnlineDictServiceImpl extends BaseService implements OnlineDictService { + + @Autowired + private OnlineDictMapper onlineDictMapper; + @Autowired + private OnlineDblinkService dblinkService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineDictMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineDict 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineDict saveNew(OnlineDict onlineDict) { + onlineDict.setDictId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDict.setAppCode(tokenData.getAppCode()); + Date now = new Date(); + onlineDict.setUpdateTime(now); + onlineDict.setCreateTime(now); + onlineDict.setCreateUserId(tokenData.getUserId()); + onlineDict.setUpdateUserId(tokenData.getUserId()); + onlineDictMapper.insert(onlineDict); + return onlineDict; + } + + /** + * 更新数据对象。 + * + * @param onlineDict 更新的对象。 + * @param originalOnlineDict 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineDict onlineDict, OnlineDict originalOnlineDict) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDictKey(onlineDict.getDictId())); + TokenData tokenData = TokenData.takeFromRequest(); + onlineDict.setAppCode(tokenData.getAppCode()); + onlineDict.setUpdateTime(new Date()); + onlineDict.setUpdateUserId(tokenData.getUserId()); + onlineDict.setCreateTime(originalOnlineDict.getCreateTime()); + onlineDict.setCreateUserId(originalOnlineDict.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(onlineDict, onlineDict.getDictId()); + return onlineDictMapper.update(onlineDict, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param dictId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long dictId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineDictKey(dictId)); + return onlineDictMapper.deleteById(dictId) == 1; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineDictListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDictList(OnlineDict filter, String orderBy) { + if (filter == null) { + filter = new OnlineDict(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineDictMapper.getOnlineDictList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineDictList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineDictListWithRelation(OnlineDict filter, String orderBy) { + List resultList = this.getOnlineDictList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + @Override + public OnlineDict getOnlineDictFromCache(Long dictId) { + String key = OnlineRedisKeyUtil.makeOnlineDictKey(dictId); + return commonRedisUtil.getFromCache(key, dictId, this::getById, OnlineDict.class); + } + + @Override + public List getOnlineDictListFromCache(Set dictIdSet) { + List dictList = new LinkedList<>(); + dictIdSet.forEach(dictId -> { + OnlineDict dict = this.getOnlineDictFromCache(dictId); + if (dict != null) { + dictList.add(dict); + } + }); + return dictList; + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param onlineDict 最新数据对象。 + * @param originalOnlineDict 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData(OnlineDict onlineDict, OnlineDict originalOnlineDict) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + //这里是基于字典的验证。 + if (this.needToVerify(onlineDict, originalOnlineDict, OnlineDict::getDblinkId) + && !dblinkService.existId(onlineDict.getDblinkId())) { + return CallResult.error(String.format(errorMessageFormat, "数据库链接主键id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java new file mode 100644 index 00000000..60b92227 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineFormServiceImpl.java @@ -0,0 +1,313 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineFormDatasourceMapper; +import com.orangeforms.common.online.dao.OnlineFormMapper; +import com.orangeforms.common.online.model.OnlineForm; +import com.orangeforms.common.online.model.OnlineFormDatasource; +import com.orangeforms.common.online.service.OnlineFormService; +import com.orangeforms.common.online.service.OnlinePageService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.github.pagehelper.Page; +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.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线表单数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineFormService") +public class OnlineFormServiceImpl extends BaseService implements OnlineFormService { + + @Autowired + private OnlineFormMapper onlineFormMapper; + @Autowired + private OnlineFormDatasourceMapper onlineFormDatasourceMapper; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlinePageService onlinePageService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private RedissonClient redissonClient; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineFormMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineForm 新增对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineForm saveNew(OnlineForm onlineForm, Set datasourceIdSet) { + onlineForm.setFormId(idGenerator.nextLongId()); + TokenData tokenData = TokenData.takeFromRequest(); + onlineForm.setAppCode(tokenData.getAppCode()); + onlineForm.setTenantId(tokenData.getTenantId()); + Date now = new Date(); + onlineForm.setUpdateTime(now); + onlineForm.setCreateTime(now); + onlineForm.setCreateUserId(tokenData.getUserId()); + onlineForm.setUpdateUserId(tokenData.getUserId()); + onlineFormMapper.insert(onlineForm); + if (CollUtil.isNotEmpty(datasourceIdSet)) { + for (Long datasourceId : datasourceIdSet) { + OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource(); + onlineFormDatasource.setId(idGenerator.nextLongId()); + onlineFormDatasource.setFormId(onlineForm.getFormId()); + onlineFormDatasource.setDatasourceId(datasourceId); + onlineFormDatasourceMapper.insert(onlineFormDatasource); + } + } + return onlineForm; + } + + /** + * 更新数据对象。 + * + * @param onlineForm 更新的对象。 + * @param originalOnlineForm 原有数据对象。 + * @param datasourceIdSet 在线表单关联的数据源Id集合。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineForm onlineForm, OnlineForm originalOnlineForm, Set datasourceIdSet) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(onlineForm.getFormId())); + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(onlineForm.getFormId())); + TokenData tokenData = TokenData.takeFromRequest(); + onlineForm.setAppCode(tokenData.getAppCode()); + onlineForm.setTenantId(tokenData.getTenantId()); + onlineForm.setUpdateTime(new Date()); + onlineForm.setUpdateUserId(tokenData.getUserId()); + onlineForm.setCreateTime(originalOnlineForm.getCreateTime()); + onlineForm.setCreateUserId(originalOnlineForm.getCreateUserId()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(onlineForm, onlineForm.getFormId()); + if (onlineFormMapper.update(onlineForm, uw) != 1) { + return false; + } + OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource(); + formDatasourceFilter.setFormId(onlineForm.getFormId()); + onlineFormDatasourceMapper.delete(new QueryWrapper<>(formDatasourceFilter)); + if (CollUtil.isNotEmpty(datasourceIdSet)) { + for (Long datasourceId : datasourceIdSet) { + OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource(); + onlineFormDatasource.setId(idGenerator.nextLongId()); + onlineFormDatasource.setFormId(onlineForm.getFormId()); + onlineFormDatasource.setDatasourceId(datasourceId); + onlineFormDatasourceMapper.insert(onlineFormDatasource); + } + } + return true; + } + + /** + * 删除指定数据。 + * + * @param formId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long formId) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(formId)); + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId)); + if (onlineFormMapper.deleteById(formId) != 1) { + return false; + } + OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource(); + formDatasourceFilter.setFormId(formId); + onlineFormDatasourceMapper.delete(new QueryWrapper<>(formDatasourceFilter)); + return true; + } + + /** + * 根据PageId,删除其所属的所有表单,以及表单关联的数据源数据。 + * + * @param pageId 指定的pageId。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int removeByPageId(Long pageId) { + OnlineForm filter = new OnlineForm(); + filter.setPageId(pageId); + List formList = onlineFormMapper.selectList(new QueryWrapper<>(filter)); + Set formIdSet = formList.stream().map(OnlineForm::getFormId).collect(Collectors.toSet()); + if (CollUtil.isNotEmpty(formIdSet)) { + onlineFormDatasourceMapper.delete( + new QueryWrapper().lambda().in(OnlineFormDatasource::getFormId, formIdSet)); + for (Long formId : formIdSet) { + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(formId)); + commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId)); + } + } + return onlineFormMapper.delete(new QueryWrapper<>(filter)); + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineFormListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineFormList(OnlineForm filter, String orderBy) { + if (filter == null) { + filter = new OnlineForm(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlineFormMapper.getOnlineFormList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineFormList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineFormListWithRelation(OnlineForm filter, String orderBy) { + List resultList = this.getOnlineFormList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 获取使用指定数据表的表单列表。 + * + * @param tableId 数据表Id。 + * @return 使用该数据表的表单列表。 + */ + @Override + public List getOnlineFormListByTableId(Long tableId) { + OnlineForm filter = new OnlineForm(); + filter.setMasterTableId(tableId); + return this.getOnlineFormList(filter, null); + } + + @Override + public List getFormDatasourceListFromCache(Long formId) { + String key = OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId); + RBucket bucket = redissonClient.getBucket(key); + if (bucket.isExists()) { + return JSONArray.parseArray(bucket.get(), OnlineFormDatasource.class); + } + LambdaQueryWrapper queryWrapper = + new QueryWrapper().lambda().eq(OnlineFormDatasource::getFormId, formId); + List resultList = onlineFormDatasourceMapper.selectList(queryWrapper); + bucket.set(JSONArray.toJSONString(resultList)); + return resultList; + } + + /** + * 查询正在使用当前数据源的表单。 + * + * @param datasourceId 数据源Id。 + * @return 正在使用当前数据源的表单列表。 + */ + @Override + public List getOnlineFormListByDatasourceId(Long datasourceId) { + OnlineForm filter = new OnlineForm(); + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlineFormMapper.getOnlineFormListByDatasourceId(datasourceId, filter); + } + + @Override + public OnlineForm getOnlineFormFromCache(Long formId) { + String key = OnlineRedisKeyUtil.makeOnlineFormKey(formId); + return commonRedisUtil.getFromCache(key, formId, this::getById, OnlineForm.class); + } + + @Override + public boolean existByFormCode(String formCode) { + OnlineForm filter = new OnlineForm(); + filter.setFormCode(formCode); + return CollUtil.isNotEmpty(this.getOnlineFormList(filter, null)); + } + + @Override + public List getOnlineFormListByPageIds(Set pageIdSet) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(OnlineForm::getPageId, pageIdSet); + return onlineFormMapper.selectList(queryWrapper); + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param onlineForm 最新数据对象。 + * @param originalOnlineForm 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData(OnlineForm onlineForm, OnlineForm originalOnlineForm) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + //这里是基于字典的验证。 + if (this.needToVerify(onlineForm, originalOnlineForm, OnlineForm::getMasterTableId) + && !onlineTableService.existId(onlineForm.getMasterTableId())) { + return CallResult.error(String.format(errorMessageFormat, "表单主表id")); + } + //这里是一对多的验证 + if (this.needToVerify(onlineForm, originalOnlineForm, OnlineForm::getPageId) + && !onlinePageService.existId(onlineForm.getPageId())) { + return CallResult.error(String.format(errorMessageFormat, "页面id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java new file mode 100644 index 00000000..8dde618f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineOperationServiceImpl.java @@ -0,0 +1,1759 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.github.pagehelper.page.PageMethod; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.orangeforms.common.core.annotation.MultiDatabaseWriteMethod; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.*; +import com.orangeforms.common.core.exception.NoDataPermException; +import com.orangeforms.common.core.object.*; +import com.orangeforms.common.core.util.*; +import com.orangeforms.common.datafilter.config.DataFilterProperties; +import com.orangeforms.common.dbutil.constant.DblinkType; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.dict.service.GlobalDictService; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.dao.OnlineOperationMapper; +import com.orangeforms.common.online.dto.OnlineFilterDto; +import com.orangeforms.common.online.exception.OnlineRuntimeException; +import com.orangeforms.common.online.model.*; +import com.orangeforms.common.online.model.constant.FieldFilterType; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.model.constant.VirtualType; +import com.orangeforms.common.online.object.ColumnData; +import com.orangeforms.common.online.object.ConstDictInfo; +import com.orangeforms.common.online.object.JoinTableInfo; +import com.orangeforms.common.online.service.*; +import com.orangeforms.common.online.util.*; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import jakarta.annotation.Resource; +import lombok.Data; +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.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineOperationService") +public class OnlineOperationServiceImpl implements OnlineOperationService { + + @Autowired + private OnlineOperationMapper onlineOperationMapper; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDictService onlineDictService; + @Autowired + private OnlineVirtualColumnService onlineVirtualColumnService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineOperationHelper onlineOperationHelper; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private OnlineCustomExtFactory customExtFactory; + @Autowired + private GlobalDictService globalDictService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + @Autowired + private DataFilterProperties dataFilterProperties; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + @Autowired + private OnlineDataSourceUtil dataSourceUtil; + + private static final String DICT_MAP_SUFFIX = "DictMap"; + private static final String DICT_MAP_LIST_SUFFIX = "DictMapList"; + private static final String SELECT = "SELECT "; + private static final String FROM = " FROM "; + private static final String WHERE = " WHERE "; + private static final String AND = " AND "; + + /** + * 聚合返回数据中,聚合键的常量字段名。 + * 如select groupColumn grouped_key, max(aggregationColumn) aggregated_value。 + */ + private static final String KEY_NAME = "grouped_key"; + /** + * 聚合返回数据中,聚合值的常量字段名。 + * 如select groupColumn grouped_key, max(aggregationColumn) aggregated_value。 + */ + private static final String VALUE_NAME = "aggregated_value"; + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewBatch(OnlineTable table, List dataList) { + for (JSONObject data : dataList) { + this.saveNew(table, data); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public Object saveNew(OnlineTable table, JSONObject data) { + ResponseResult> columnDataListResult = + onlineOperationHelper.buildTableData(table, data, false, null); + if (!columnDataListResult.isSuccess()) { + throw new OnlineRuntimeException(columnDataListResult.getErrorMessage()); + } + List columnDataList = columnDataListResult.getData(); + String columnNames = this.makeColumnNames(columnDataList); + List columnValueList = new LinkedList<>(); + Object id = null; + // 这里逐个处理每一行数据,特别是非自增主键、createUserId、createTime、逻辑删除等特殊属性的字段。 + for (ColumnData columnData : columnDataList) { + this.makeupColumnValue(columnData); + if (BooleanUtil.isFalse(columnData.getColumn().getAutoIncrement())) { + columnValueList.add(columnData.getColumnValue()); + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey())) { + id = columnData.getColumnValue(); + // 这里必须补齐主键值到JSON对象,后面的从表关联字段值填充可能会用到该值。 + data.put(columnData.getColumn().getColumnName(), id); + } + } + } + onlineOperationMapper.insert(table.getTableName(), columnNames, columnValueList); + return id; + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public Object saveNewWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Map> slaveDataListMap) { + Object id = this.saveNew(masterTable, masterData); + if (slaveDataListMap == null) { + return id; + } + // 迭代多个关联列表。 + for (Map.Entry> entry : slaveDataListMap.entrySet()) { + Long masterColumnId = entry.getKey().getMasterColumnId(); + OnlineColumn masterColumn = masterTable.getColumnMap().get(masterColumnId); + Object columnValue = masterData.get(masterColumn.getColumnName()); + OnlineTable slaveTable = entry.getKey().getSlaveTable(); + OnlineColumn slaveColumn = slaveTable.getColumnMap().get(entry.getKey().getSlaveColumnId()); + // 迭代关联中的数据集合 + for (JSONObject slaveData : entry.getValue()) { + if (!slaveData.containsKey(slaveTable.getPrimaryKeyColumn().getColumnName())) { + slaveData.put(slaveColumn.getColumnName(), columnValue); + this.saveNew(slaveTable, slaveData); + } + } + } + return id; + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineTable table, JSONObject data) { + ResponseResult> columnDataListResult = + onlineOperationHelper.buildTableData(table, data, true, null); + if (!columnDataListResult.isSuccess()) { + throw new OnlineRuntimeException(columnDataListResult.getErrorMessage()); + } + List columnDataList = columnDataListResult.getData(); + String tableName = table.getTableName(); + List updateColumnList = new LinkedList<>(); + List filterList = new LinkedList<>(); + String dataId = null; + for (ColumnData columnData : columnDataList) { + this.makeupColumnValue(columnData); + // 对于以下几种类型的字段,忽略更新。 + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey()) + || ObjectUtil.equal(columnData.getColumn().getFieldKind(), FieldKind.LOGIC_DELETE)) { + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(tableName); + filter.setColumnName(columnData.getColumn().getColumnName()); + filter.setColumnValue(columnData.getColumnValue()); + filterList.add(filter); + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey())) { + dataId = columnData.getColumnValue().toString(); + } + continue; + } + if (!MyCommonUtil.equalsAny(columnData.getColumn().getFieldKind(), + FieldKind.CREATE_TIME, FieldKind.CREATE_USER_ID, FieldKind.CREATE_DEPT_ID, FieldKind.TENANT_FILTER)) { + updateColumnList.add(columnData); + } + } + if (CollUtil.isEmpty(updateColumnList)) { + return true; + } + String dataPermFilter = this.buildDataPermFilter(table); + return this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateColumn(OnlineTable table, String dataId, OnlineColumn column, T dataValue) { + List updateColumnList = new LinkedList<>(); + ColumnData updateColumnData = new ColumnData(); + updateColumnData.setColumn(column); + updateColumnData.setColumnValue(dataValue); + updateColumnList.add(updateColumnData); + List filterList = this.makeDefaultFilter(table, table.getPrimaryKeyColumn(), dataId); + String dataPermFilter = this.buildDataPermFilter(table); + return this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void updateWithRelation( + OnlineTable masterTable, + JSONObject masterData, + Long datasourceId, + Map> slaveDataListMap) { + this.update(masterTable, masterData); + if (slaveDataListMap == null) { + return; + } + String masterDataId = masterData.get(masterTable.getPrimaryKeyColumn().getColumnName()).toString(); + for (Map.Entry> relationEntry : slaveDataListMap.entrySet()) { + Long relationId = relationEntry.getKey().getRelationId(); + this.updateRelationData( + masterTable, masterData, masterDataId, datasourceId, relationId, relationEntry.getValue()); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void updateRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + Long datasourceId, + Long relationId, + List slaveDataList) { + ResponseResult relationResult = + onlineOperationHelper.verifyAndGetRelation(datasourceId, relationId); + if (!relationResult.isSuccess()) { + throw new OnlineRuntimeException(relationResult.getErrorMessage()); + } + OnlineDatasourceRelation relation = relationResult.getData(); + OnlineTable slaveTable = relation.getSlaveTable(); + if (relation.getRelationType().equals(RelationType.ONE_TO_ONE)) { + JSONObject slaveData = null; + if (CollUtil.isNotEmpty(slaveDataList)) { + slaveData = slaveDataList.get(0); + } + this.saveNewOrUpdateOneToOneRelationData( + masterTable, masterData, masterDataId, slaveTable, slaveData, relation); + } else if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + if (slaveDataList == null) { + return; + } + this.saveNewOrUpdateOneToManyRelationData( + masterTable, masterData, masterDataId, slaveTable, slaveDataList, relation); + } + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public boolean delete(OnlineTable table, List relationList, String dataId) { + List filterList = + this.makeDefaultFilter(table, table.getPrimaryKeyColumn(), dataId); + String dataPermFilter = this.buildDataPermFilter(table); + if (table.getLogicDeleteColumn() == null) { + if (this.doDelete(table, filterList, dataPermFilter) != 1) { + return false; + } + } else { + this.doLogicDelete(table, table.getPrimaryKeyColumn(), dataId, dataPermFilter); + } + if (CollUtil.isEmpty(relationList)) { + return true; + } + Map masterData = getMasterData(table, null, null, dataId); + for (OnlineDatasourceRelation relation : relationList) { + if (BooleanUtil.isFalse(relation.getCascadeDelete())) { + continue; + } + OnlineTable slaveTable = relation.getSlaveTable(); + OnlineColumn slaveColumn = + relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + String columnValue = dataId; + if (!relation.getMasterColumnId().equals(table.getPrimaryKeyColumn().getColumnId())) { + OnlineColumn relationMasterColumn = table.getColumnMap().get(relation.getMasterColumnId()); + columnValue = masterData.get(relationMasterColumn.getColumnName()).toString(); + } + List slaveFilterList = + this.makeDefaultFilter(relation.getSlaveTable(), slaveColumn, columnValue); + if (slaveTable.getLogicDeleteColumn() == null) { + this.doDelete(slaveTable, slaveFilterList, null); + } else { + this.doLogicDelete(slaveTable, slaveColumn, columnValue, null); + } + } + return true; + } + + @MultiDatabaseWriteMethod + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteOneToManySlaveData( + OnlineTable table, OnlineColumn column, String columnValue, Set keptIdSet) { + List filterList = this.makeDefaultFilter(table, column, columnValue); + if (CollUtil.isNotEmpty(keptIdSet)) { + OnlineFilterDto keptIdSetFilter = new OnlineFilterDto(); + Set convertedIdSet = + onlineOperationHelper.convertToTypeValue(table.getPrimaryKeyColumn(), keptIdSet); + keptIdSetFilter.setColumnValueList(new HashSet<>(convertedIdSet)); + keptIdSetFilter.setTableName(table.getTableName()); + keptIdSetFilter.setColumnName(table.getPrimaryKeyColumn().getColumnName()); + keptIdSetFilter.setFilterType(FieldFilterType.NOT_IN_LIST_FILTER); + filterList.add(keptIdSetFilter); + } + if (table.getLogicDeleteColumn() == null) { + this.doDelete(table, filterList, null); + } else { + this.doLogicDelete(table, filterList, null); + } + } + + @Override + public boolean existId(OnlineTable table, String dataId) { + return this.getMasterData(table, null, null, dataId) != null; + } + + @Override + public Map getMasterData( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + String dataId) { + List filterList = + this.makeDefaultFilter(table, table.getPrimaryKeyColumn(), dataId); + // 组件表关联数据。 + List joinInfoList = this.makeJoinInfoList(table, oneToOneRelationList); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFieldsWithRelation(table, oneToOneRelationList); + String dataPermFilter = this.buildDataPermFilter(table); + this.normalizeFiltersSlaveTableAlias(oneToOneRelationList, filterList); + selectFields = this.normalizeSlaveTableAlias(oneToOneRelationList, selectFields); + MyPageData> pageData = this.getList( + table, joinInfoList, selectFields, filterList, dataPermFilter, null, null); + List> resultList = pageData.getDataList(); + this.buildDataListWithDict(resultList, table, oneToOneRelationList); + if (CollUtil.isEmpty(resultList)) { + return null; + } + if (CollUtil.isNotEmpty(allRelationList)) { + // 针对一对多和多对多关联,计算虚拟聚合字段。 + List toManyRelationList = allRelationList.stream() + .filter(r -> !r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + this.buildVirtualColumn(resultList, table, toManyRelationList); + } + this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); + return resultList.get(0); + } + + @Override + public Map getSlaveData(OnlineDatasourceRelation relation, String dataId) { + OnlineTable slaveTable = relation.getSlaveTable(); + List filterList = + this.makeDefaultFilter(slaveTable, slaveTable.getPrimaryKeyColumn(), dataId); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFields(slaveTable, null); + String dataPermFilter = this.buildDataPermFilter(slaveTable); + MyPageData> pageData = this.getList( + slaveTable, null, selectFields, filterList, dataPermFilter, null, null); + List> resultList = pageData.getDataList(); + this.buildDataListWithDict(resultList, slaveTable); + return CollUtil.isEmpty(resultList) ? null : resultList.get(0); + } + + @Override + public MyPageData> getMasterDataList( + OnlineTable table, + List oneToOneRelationList, + List allRelationList, + List filterList, + String orderBy, + MyPageParam pageParam) { + this.normalizeFilterList(table, oneToOneRelationList, filterList); + // 组件表关联数据。 + List joinInfoList = this.makeJoinInfoList(table, oneToOneRelationList); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFieldsWithRelation(table, oneToOneRelationList); + String dataPermFilter = this.buildDataPermFilter(table); + this.normalizeFiltersSlaveTableAlias(oneToOneRelationList, filterList); + selectFields = this.normalizeSlaveTableAlias(oneToOneRelationList, selectFields); + orderBy = this.normalizeSlaveTableAlias(oneToOneRelationList, orderBy); + MyPageData> pageData = + this.getList(table, joinInfoList, selectFields, filterList, dataPermFilter, orderBy, pageParam); + List> resultList = pageData.getDataList(); + this.buildDataListWithDict(resultList, table, oneToOneRelationList); + // 针对一对多和多对多关联,计算虚拟聚合字段。 + if (CollUtil.isNotEmpty(allRelationList)) { + List toManyRelationList = allRelationList.stream() + .filter(r -> !r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); + this.buildVirtualColumn(resultList, table, toManyRelationList); + } + this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); + return pageData; + } + + @Override + public MyPageData> getSlaveDataList( + OnlineDatasourceRelation relation, List filterList, String orderBy, MyPageParam pageParam) { + OnlineTable slaveTable = relation.getSlaveTable(); + this.normalizeFilterList(slaveTable, null, filterList); + // 拼接关联表的select fields字段。 + String selectFields = this.makeSelectFields(slaveTable, null); + String dataPermFilter = this.buildDataPermFilter(slaveTable); + MyPageData> pageData = + this.getList(slaveTable, null, selectFields, filterList, dataPermFilter, orderBy, pageParam); + this.buildDataListWithDict(pageData.getDataList(), slaveTable); + return pageData; + } + + @Override + public List> getDictDataList(OnlineDict dict, List filterList) { + if (StrUtil.isNotBlank(dict.getDeletedColumnName())) { + if (filterList == null) { + filterList = new LinkedList<>(); + } + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setColumnName(dict.getDeletedColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + if (StrUtil.isNotBlank(dict.getTenantFilterColumnName())) { + if (filterList == null) { + filterList = new LinkedList<>(); + } + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setColumnName(dict.getTenantFilterColumnName()); + filter.setColumnValue(TokenData.takeFromRequest().getTenantId()); + filterList.add(filter); + } + String selectFields = this.makeDictSelectFields(dict, false); + String dataPermFilter = this.buildDataPermFilter( + dict.getTableName(), dict.getDeptFilterColumnName(), dict.getUserFilterColumnName()); + return this.getDictList(dict.getDblinkId(), dict.getTableName(), selectFields, filterList, dataPermFilter); + } + + @Override + public void buildDataListWithDict( + OnlineTable masterTable, List relationList, List> dataList) { + this.buildDataListWithDict(dataList, masterTable, relationList); + } + + @Override + public Map calculatePermData(Set menuFormIds, Set viewFormIds, Set editFormIds) { + Map> formMenuPermMap = new HashMap<>(menuFormIds.size()); + for (Long menuFormId : menuFormIds) { + formMenuPermMap.put(menuFormId, new HashSet<>()); + } + Set permCodeSet = new HashSet<>(10); + Set permUrlSet = new HashSet<>(10); + if (CollUtil.isNotEmpty(viewFormIds)) { + List datasourceList = + onlineDatasourceService.getOnlineDatasourceListByFormIds(viewFormIds); + for (OnlineDatasource datasource : datasourceList) { + permCodeSet.add(OnlineUtil.makeViewPermCode(datasource.getVariableName())); + Set permUrls = onlineProperties.getViewUrlList().stream() + .map(url -> url + datasource.getVariableName()).collect(Collectors.toSet()); + permUrlSet.addAll(permUrls); + datasource.getOnlineFormDatasourceList().forEach(formDatasource -> + formMenuPermMap.get(formDatasource.getFormId()).addAll(permUrls)); + } + } + if (CollUtil.isNotEmpty(editFormIds)) { + List datasourceList = + onlineDatasourceService.getOnlineDatasourceListByFormIds(editFormIds); + for (OnlineDatasource datasource : datasourceList) { + permCodeSet.add(OnlineUtil.makeEditPermCode(datasource.getVariableName())); + Set permUrls = onlineProperties.getEditUrlList().stream() + .map(url -> url + datasource.getVariableName()).collect(Collectors.toSet()); + permUrlSet.addAll(permUrls); + datasource.getOnlineFormDatasourceList().forEach(formDatasource -> + formMenuPermMap.get(formDatasource.getFormId()).addAll(permUrls)); + } + } + List onlineWhitelistUrls = CollUtil.newArrayList( + onlineProperties.getUrlPrefix() + "/onlineOperation/listDict", + onlineProperties.getUrlPrefix() + "/onlineForm/render", + onlineProperties.getUrlPrefix() + "/onlineForm/view"); + Map resultMap = new HashMap<>(3); + resultMap.put("permCodeSet", permCodeSet); + resultMap.put("permUrlSet", permUrlSet); + resultMap.put("formMenuPermMap", formMenuPermMap); + resultMap.put("onlineWhitelistUrls", onlineWhitelistUrls); + return resultMap; + } + + private boolean doUpdate( + OnlineTable table, List updateColumns, List filters, String dataPermFilter) { + return onlineOperationMapper.update(table.getTableName(), updateColumns, filters, dataPermFilter) == 1; + } + + private int doDelete(OnlineTable table, List filters, String dataPermFilter) { + return onlineOperationMapper.delete(table.getTableName(), filters, dataPermFilter); + } + + private List> getGroupedListByCondition( + Long dblinkId, String selectTable, String selectFields, String whereClause, String groupBy) { + return onlineOperationMapper.getGroupedListByCondition(selectTable, selectFields, whereClause, groupBy); + } + + private List> getDictList( + Long dblinkId, String tableName, String selectFields, List filterList, String dataPermFilter) { + return onlineOperationMapper.getDictList(tableName, selectFields, filterList, dataPermFilter); + } + + private MyPageData> getList( + OnlineTable table, + List joinInfoList, + String selectFields, + List filterList, + String dataPermFilter, + String orderBy, + MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + List> resultList = onlineOperationMapper.getList( + table.getTableName(), joinInfoList, selectFields, filterList, dataPermFilter, orderBy); + return MyPageUtil.makeResponseData(resultList); + } + + private String makeWhereClause(List filters, String dataPermFilter, List paramList) { + if (CollUtil.isEmpty(filters) && StrUtil.isBlank(dataPermFilter)) { + return ""; + } + StringBuilder where = new StringBuilder(512); + List normalizedFilters = new LinkedList<>(); + if (CollUtil.isNotEmpty(filters)) { + for (OnlineFilterDto filter : filters) { + String filterString = this.makeSubWhereClause(filter, paramList); + if (StrUtil.isNotBlank(filterString)) { + normalizedFilters.add(filterString); + } + } + } + if (CollUtil.isNotEmpty(normalizedFilters)) { + where.append(WHERE); + where.append(CollUtil.join(normalizedFilters, AND)); + } + if (StrUtil.isNotBlank(dataPermFilter)) { + if (CollUtil.isNotEmpty(normalizedFilters)) { + where.append(AND); + } else { + where.append(WHERE); + } + where.append(dataPermFilter); + } + return where.toString(); + } + + private String makeSubWhereClause(OnlineFilterDto filter, List paramList) { + StringBuilder where = new StringBuilder(256); + if (filter.getFilterType().equals(FieldFilterType.EQUAL_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" = ? "); + paramList.add(filter.getColumnValue()); + } else if (filter.getFilterType().equals(FieldFilterType.RANGE_FILTER)) { + where.append(this.makeRangeFilterClause(filter, paramList)); + } else if (filter.getFilterType().equals(FieldFilterType.LIKE_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" LIKE ? "); + paramList.add(filter.getColumnValue()); + } else if (filter.getFilterType().equals(FieldFilterType.IN_LIST_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" IN ( "); + where.append(StrUtil.repeat("?,", filter.getColumnValueList().size())); + where.setLength(where.length() - 1); + where.append(")"); + paramList.addAll(filter.getColumnValueList()); + } else if (filter.getFilterType().equals(FieldFilterType.MULTI_LIKE)) { + where.append("("); + StringBuilder sb = new StringBuilder(128); + sb.append(this.makeWhereLeftOperator(filter)).append(" LIKE ? OR "); + String s = StrUtil.repeat(sb.toString(), filter.getColumnValueList().size()); + where.append(s, 0, s.length() - 4); + where.append(")"); + paramList.addAll(filter.getColumnValueList()); + } else if (filter.getFilterType().equals(FieldFilterType.NOT_IN_LIST_FILTER)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" NOT IN ("); + where.append(StrUtil.repeat("?,", filter.getColumnValueList().size())); + where.setLength(where.length() - 1); + where.append(")"); + paramList.addAll(filter.getColumnValueList()); + } else if (filter.getFilterType().equals(FieldFilterType.IS_NULL)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" IS NULL "); + } else if (filter.getFilterType().equals(FieldFilterType.IS_NOT_NULL)) { + where.append(this.makeWhereLeftOperator(filter)); + where.append(" IS NOT NULL "); + } + return where.toString(); + } + + private String makeRangeFilterClause(OnlineFilterDto filter, List paramList) { + StringBuilder where = new StringBuilder(256); + if (ObjectUtil.isNotEmpty(filter.getColumnValueStart())) { + where.append(this.makeWhereLeftOperator(filter)); + if (BooleanUtil.isTrue(filter.getIsOracleDate())) { + where.append(" >= ").append(filter.getColumnValueStart()); + } else { + where.append(" >= ? "); + paramList.add(filter.getColumnValueStart()); + } + } + if (ObjectUtil.isNotEmpty(filter.getColumnValueEnd())) { + if (ObjectUtil.isNotEmpty(filter.getColumnValueStart())) { + where.append(AND); + } + where.append(this.makeWhereLeftOperator(filter)); + if (BooleanUtil.isTrue(filter.getIsOracleDate())) { + where.append(" <= ").append(filter.getColumnValueEnd()); + } else { + where.append(" <= ? "); + paramList.add(filter.getColumnValueEnd()); + } + } + return where.toString(); + } + + private String makeWhereLeftOperator(OnlineFilterDto filter) { + if (StrUtil.isBlank(filter.getTableName())) { + return filter.getColumnName(); + } + StringBuilder sb = new StringBuilder(128); + sb.append(filter.getTableName()).append(".").append(filter.getColumnName()); + return sb.toString(); + } + + private void saveNewOrUpdateOneToManyRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + OnlineTable slaveTable, + List relationDataList, + OnlineDatasourceRelation relation) { + if (masterData == null) { + masterData = this.getMasterData(masterTable, null, null, masterDataId); + } + Set idSet = new HashSet<>(relationDataList.size()); + for (JSONObject relationData : relationDataList) { + Object id = relationData.get(relation.getSlaveTable().getPrimaryKeyColumn().getColumnName()); + if (ObjectUtil.isNotEmpty(id)) { + idSet.add(id.toString()); + } + } + // 自动补齐主表关联数据。 + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + Object masterColumnValue = masterData.get(masterColumn.getColumnName()); + OnlineColumn slaveColumn = relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + // 在从表中删除本地批量更新不存在的数据。 + this.deleteOneToManySlaveData( + relation.getSlaveTable(), slaveColumn, masterColumnValue.toString(), idSet); + for (JSONObject relationData : relationDataList) { + // 自动补齐主表关联数据。 + relationData.put(slaveColumn.getColumnName(), masterColumnValue); + // 拆解主表和一对多关联从表的输入参数,并构建出数据表的待插入数据列表。 + Object id = relationData.get(relation.getSlaveTable().getPrimaryKeyColumn().getColumnName()); + if (id == null) { + this.saveNew(slaveTable, relationData); + } else { + this.update(slaveTable, relationData); + } + } + } + + private void saveNewOrUpdateOneToOneRelationData( + OnlineTable masterTable, + Map masterData, + String masterDataId, + OnlineTable slaveTable, + JSONObject slaveData, + OnlineDatasourceRelation relation) { + if (MapUtil.isEmpty(slaveData)) { + return; + } + String keyColumnName = slaveTable.getPrimaryKeyColumn().getColumnName(); + String slaveDataId = slaveData.getString(keyColumnName); + if (slaveDataId == null) { + if (masterData == null) { + masterData = this.getMasterData(masterTable, null, null, masterDataId); + } + // 自动补齐主表关联数据。 + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + Object masterColumnValue = masterData.get(masterColumn.getColumnName()); + OnlineColumn slaveColumn = slaveTable.getColumnMap().get(relation.getSlaveColumnId()); + slaveData.put(slaveColumn.getColumnName(), masterColumnValue); + this.saveNew(slaveTable, slaveData); + } else { + Map originalSlaveData = + this.getMasterData(slaveTable, null, null, slaveDataId); + for (Map.Entry entry : originalSlaveData.entrySet()) { + slaveData.putIfAbsent(entry.getKey(), entry.getValue()); + } + if (!this.update(slaveTable, slaveData)) { + throw new OnlineRuntimeException("关联从表 [" + slaveTable.getTableName() + "] 中的更新数据不存在"); + } + } + } + + private void reformatResultListWithOneToOneRelation( + List> resultList, List oneToOneRelationList) { + if (CollUtil.isEmpty(oneToOneRelationList) || CollUtil.isEmpty(resultList)) { + return; + } + for (OnlineDatasourceRelation r : oneToOneRelationList) { + for (Map resultMap : resultList) { + Collection slaveColumnList = r.getSlaveTable().getColumnMap().values(); + Map oneToOneRelationDataMap = new HashMap<>(slaveColumnList.size()); + resultMap.put(r.getVariableName(), oneToOneRelationDataMap); + for (OnlineColumn c : slaveColumnList) { + StringBuilder sb = new StringBuilder(64); + sb.append(r.getVariableName()) + .append(OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR).append(c.getColumnName()); + Object data = this.removeRelationColumnData(resultMap, sb.toString()); + oneToOneRelationDataMap.put(c.getColumnName(), data); + if (c.getDictId() != null) { + sb.append(DICT_MAP_SUFFIX); + data = this.removeRelationColumnData(resultMap, sb.toString()); + oneToOneRelationDataMap.put(c.getColumnName() + DICT_MAP_SUFFIX, data); + } + } + } + } + } + + private Object removeRelationColumnData(Map resultMap, String name) { + Object data = resultMap.remove(name); + if (data == null) { + data = resultMap.remove("\"" + name + "\""); + } + return data; + } + + private void buildVirtualColumn( + List> resultList, OnlineTable table, List relationList) { + if (CollUtil.isEmpty(resultList) || CollUtil.isEmpty(relationList)) { + return; + } + OnlineVirtualColumn virtualColumnFilter = new OnlineVirtualColumn(); + virtualColumnFilter.setTableId(table.getTableId()); + virtualColumnFilter.setVirtualType(VirtualType.AGGREGATION); + List virtualColumnList = + onlineVirtualColumnService.getOnlineVirtualColumnList(virtualColumnFilter, null); + if (CollUtil.isEmpty(virtualColumnList)) { + return; + } + Map relationMap = + relationList.stream().collect(Collectors.toMap(OnlineDatasourceRelation::getRelationId, r -> r)); + for (OnlineVirtualColumn virtualColumn : virtualColumnList) { + OnlineDatasourceRelation relation = relationMap.get(virtualColumn.getRelationId()); + if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + this.doBuildVirtualColumnForOneToMany(table, resultList, virtualColumn, relation); + } + } + } + + private void doBuildVirtualColumnForOneToMany( + OnlineTable masterTable, + List> resultList, + OnlineVirtualColumn virtualColumn, + OnlineDatasourceRelation relation) { + String slaveTableName = relation.getSlaveTable().getTableName(); + OnlineColumn slaveColumn = + relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + String slaveColumnName = slaveColumn.getColumnName(); + OnlineColumn aggregationColumn = + relation.getSlaveTable().getColumnMap().get(virtualColumn.getAggregationColumnId()); + String aggregationColumnName = aggregationColumn.getColumnName(); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTableName, slaveColumnName, slaveTableName, aggregationColumnName, virtualColumn.getAggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + // 开始组装过滤从句。 + List criteriaList = new LinkedList<>(); + // 1. 组装主表数据对从表的过滤条件。 + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + OnlineColumn masterColumn = masterTable.getColumnMap().get(relation.getMasterColumnId()); + String masterColumnName = masterColumn.getColumnName(); + Set masterIdSet = resultList.stream() + .map(r -> r.get(masterColumnName)).filter(Objects::nonNull).collect(Collectors.toSet()); + inlistFilter.setCriteria( + slaveTableName, slaveColumnName, slaveColumn.getObjectFieldType(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + // 2. 从表逻辑删除字段过滤。 + if (relation.getSlaveTable().getLogicDeleteColumn() != null) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + slaveTableName, + relation.getSlaveTable().getLogicDeleteColumn().getColumnName(), + relation.getSlaveTable().getLogicDeleteColumn().getObjectFieldType(), + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (StrUtil.isNotBlank(virtualColumn.getWhereClauseJson())) { + List whereClauseList = + JSONArray.parseArray(virtualColumn.getWhereClauseJson(), VirtualColumnWhereClause.class); + if (CollUtil.isNotEmpty(whereClauseList)) { + for (VirtualColumnWhereClause whereClause : whereClauseList) { + MyWhereCriteria whereClauseFilter = new MyWhereCriteria(); + OnlineColumn c = relation.getSlaveTable().getColumnMap().get(whereClause.getColumnId()); + whereClauseFilter.setCriteria( + slaveTableName, + c.getColumnName(), + c.getObjectFieldType(), + whereClause.getOperatorType(), + whereClause.getValue()); + criteriaList.add(whereClauseFilter); + } + } + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + List> aggregationMapList = + getGroupedListByCondition(masterTable.getDblinkId(), slaveTableName, selectList, criteriaString, groupBy); + this.doMakeAggregationData(resultList, aggregationMapList, masterColumnName, virtualColumn.getObjectFieldName()); + } + + private void doMakeAggregationData( + List> resultList, + List> aggregationMapList, + String masterColumnName, + String virtualColumnName) { + // 根据获取的分组聚合结果集,绑定到主表总的关联字段。 + if (CollUtil.isEmpty(aggregationMapList)) { + return; + } + Map relatedMap = new HashMap<>(aggregationMapList.size()); + for (Map map : aggregationMapList) { + relatedMap.put(map.get(KEY_NAME).toString(), map.get(VALUE_NAME)); + } + for (Map dataObject : resultList) { + String masterIdValue = dataObject.get(masterColumnName).toString(); + if (masterIdValue != null) { + Object value = relatedMap.get(masterIdValue); + if (value != null) { + dataObject.put(virtualColumnName, value); + } + } + } + } + + private Tuple2 makeSelectListAndGroupByClause( + String groupTableName, + String groupColumnName, + String aggregationTableName, + String aggregationColumnName, + Integer aggregationType) { + String aggregationFunc = AggregationType.getAggregationFunction(aggregationType); + // 构建Select List + // 如:r_table.master_id groupedKey, SUM(r_table.aggr_column) aggregated_value + StringBuilder groupedSelectList = new StringBuilder(128); + groupedSelectList.append(groupTableName) + .append(".") + .append(groupColumnName) + .append(" ") + .append(KEY_NAME) + .append(", ") + .append(aggregationFunc) + .append("(") + .append(aggregationTableName) + .append(".") + .append(aggregationColumnName) + .append(") ") + .append(VALUE_NAME) + .append(" "); + StringBuilder groupBy = new StringBuilder(64); + groupBy.append(groupTableName).append(".").append(groupColumnName); + return new Tuple2<>(groupedSelectList.toString(), groupBy.toString()); + } + + private void buildDataListWithDict(List> resultList, OnlineTable slaveTable) { + if (CollUtil.isEmpty(resultList)) { + return; + } + Set dictIdSet = new HashSet<>(); + // 先找主表字段对字典的依赖。 + Multimap dictColumnMap = LinkedHashMultimap.create(); + for (OnlineColumn column : slaveTable.getColumnMap().values()) { + if (column.getDictId() != null) { + dictIdSet.add(column.getDictId()); + column.setColumnAliasName(column.getColumnName()); + dictColumnMap.put(column.getDictId(), column); + } + } + this.doBuildDataListWithDict(resultList, dictIdSet, dictColumnMap); + } + + private void buildDataListWithDict( + List> resultList, + OnlineTable masterTable, + List relationList) { + if (CollUtil.isEmpty(resultList)) { + return; + } + Set dictIdSet = new HashSet<>(); + // 先找主表字段对字典的依赖。 + Multimap dictColumnMap = LinkedHashMultimap.create(); + for (OnlineColumn column : masterTable.getColumnMap().values()) { + if (column.getDictId() != null) { + dictIdSet.add(column.getDictId()); + column.setColumnAliasName(column.getColumnName()); + dictColumnMap.put(column.getDictId(), column); + } + } + // 再找关联表字段对字典的依赖。 + if (CollUtil.isEmpty(relationList)) { + this.doBuildDataListWithDict(resultList, dictIdSet, dictColumnMap); + return; + } + for (OnlineDatasourceRelation relation : relationList) { + for (OnlineColumn column : relation.getSlaveTable().getColumnMap().values()) { + if (column.getDictId() != null) { + dictIdSet.add(column.getDictId()); + String columnAliasName = relation.getVariableName() + + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR + column.getColumnName(); + column.setColumnAliasName(columnAliasName); + dictColumnMap.put(column.getDictId(), column); + } + } + } + this.doBuildDataListWithDict(resultList, dictIdSet, dictColumnMap); + } + + private void doBuildDataListWithDict( + List> resultList, Set dictIdSet, Multimap dictColumnMap) { + if (CollUtil.isEmpty(dictIdSet)) { + return; + } + List allDictList = onlineDictService.getOnlineDictListFromCache(dictIdSet); + for (OnlineDict dict : allDictList) { + Collection columnList = dictColumnMap.get(dict.getDictId()); + for (OnlineColumn column : columnList) { + Set dictIdDataSet = this.extractColumnDictIds(resultList, column); + if (CollUtil.isNotEmpty(dictIdDataSet)) { + this.doBindColumnDictData(resultList, column, dict, dictIdDataSet); + } + } + } + } + + private Set extractColumnDictValues(List> dataList, OnlineColumn column) { + Set dictValueDataSet = new HashSet<>(); + for (Map data : dataList) { + String dictValueData = (String) data.get(column.getColumnAliasName()); + if (StrUtil.isNotBlank(dictValueData)) { + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + Set dictValueDataList = StrUtil.split(dictValueData, ",") + .stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + CollUtil.addAll(dictValueDataSet, dictValueDataList); + } else { + dictValueDataSet.add(dictValueData); + } + } + } + return dictValueDataSet; + } + + private Set extractColumnDictIds(List> resultList, OnlineColumn column) { + Set dictIdDataSet = new HashSet<>(); + for (Map result : resultList) { + Object dictIdData = result.get(column.getColumnAliasName()); + if (ObjectUtil.isEmpty(dictIdData)) { + continue; + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + Set dictIdDataList = StrUtil.split(dictIdData.toString(), ",") + .stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + if (ObjectFieldType.LONG.equals(column.getObjectFieldType())) { + dictIdDataList = dictIdDataSet.stream() + .map(c -> (Serializable) Long.valueOf(c.toString())).collect(Collectors.toSet()); + } + CollUtil.addAll(dictIdDataSet, dictIdDataList); + } else { + dictIdDataSet.add((Serializable) dictIdData); + } + } + return dictIdDataSet; + } + + private Map getGlobalDictItemDictMapFromCache(String dictCode, Set itemIds) { + return globalDictService.getGlobalDictItemDictMapFromCache(dictCode, itemIds); + } + + private void doTranslateColumnDictData( + List> dataList, + OnlineColumn column, + OnlineDict dict, + Set dictValueDataSet) { + Map dictResultMap = this.doTranslateColumnDictDataMap(dict, dictValueDataSet); + for (Map data : dataList) { + String dictValueData = (String) data.get(column.getColumnAliasName()); + if (StrUtil.isBlank(dictValueData)) { + continue; + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + List dictValueDataList = StrUtil.splitTrim(dictValueData, ","); + List dictIdList = dictValueDataList.stream() + .map(dictResultMap::get).filter(Objects::nonNull).collect(Collectors.toList()); + data.put(column.getColumnAliasName(), CollUtil.join(dictIdList, ",")); + } else { + Object dictId = dictResultMap.get(dictValueData); + if (dictId != null) { + data.put(column.getColumnAliasName(), dictId); + } + } + } + } + + private Map doTranslateColumnDictDataMap(OnlineDict dict, Set dictValueDataSet) { + Map dictResultMap = new HashMap<>(dictValueDataSet.size()); + if (dict.getDictType().equals(DictType.CUSTOM)) { + ConstDictInfo dictInfo = + JSONObject.parseObject(dict.getDictDataJson(), ConstDictInfo.class); + List dictDataList = dictInfo.getDictData(); + for (ConstDictInfo.ConstDictData dictData : dictDataList) { + dictResultMap.put(dictData.getName(), dictData.getId()); + } + } else if (dict.getDictType().equals(DictType.GLOBAL_DICT)) { + Map dictDataMap = + this.getGlobalDictItemDictMapFromCache(dict.getDictCode(), null); + dictDataMap.entrySet().stream() + .filter(entry -> dictValueDataSet.contains(entry.getValue())) + .forEach(entry -> dictResultMap.put(entry.getValue(), entry.getKey())); + } else if (dict.getDictType().equals(DictType.TABLE)) { + String selectFields = this.makeDictSelectFields(dict, true); + List filterList = this.createDefaultFilter(dict); + OnlineFilterDto inlistFilter = new OnlineFilterDto(); + inlistFilter.setTableName(dict.getTableName()); + inlistFilter.setColumnName(dict.getValueColumnName()); + inlistFilter.setColumnValueList(dictValueDataSet); + inlistFilter.setFilterType(FieldFilterType.IN_LIST_FILTER); + filterList.add(inlistFilter); + List> dictResultList = + this.getDictList(dict.getDblinkId(), dict.getTableName(), selectFields, filterList, null); + if (CollUtil.isNotEmpty(dictResultList)) { + for (Map dictResult : dictResultList) { + dictResultMap.put(dictResult.get("name").toString(), dictResult.get("id")); + } + } + } else if (dict.getDictType().equals(DictType.URL)) { + this.buildUrlDictDataMap(dict, dictResultMap, false); + } + return dictResultMap; + } + + private Map doBuildColumnDictDataMap(OnlineDict dict, Set dictIdDataSet) { + Map dictResultMap = new HashMap<>(dictIdDataSet.size()); + if (dict.getDictType().equals(DictType.CUSTOM)) { + ConstDictInfo dictInfo = + JSONObject.parseObject(dict.getDictDataJson(), ConstDictInfo.class); + List dictDataList = dictInfo.getDictData(); + for (ConstDictInfo.ConstDictData dictData : dictDataList) { + dictResultMap.put(dictData.getId().toString(), dictData.getName()); + } + } else if (dict.getDictType().equals(DictType.GLOBAL_DICT)) { + Map dictDataMap = + this.getGlobalDictItemDictMapFromCache(dict.getDictCode(), dictIdDataSet); + for (Map.Entry entry : dictDataMap.entrySet()) { + dictResultMap.put(entry.getKey().toString(), entry.getValue()); + } + } else if (dict.getDictType().equals(DictType.TABLE)) { + String selectFields = this.makeDictSelectFields(dict, true); + List filterList = this.createDefaultFilter(dict); + OnlineFilterDto inlistFilter = new OnlineFilterDto(); + inlistFilter.setTableName(dict.getTableName()); + inlistFilter.setColumnName(dict.getKeyColumnName()); + inlistFilter.setColumnValueList(dictIdDataSet); + inlistFilter.setFilterType(FieldFilterType.IN_LIST_FILTER); + filterList.add(inlistFilter); + List> dictResultList = + this.getDictList(dict.getDblinkId(), dict.getTableName(), selectFields, filterList, null); + if (CollUtil.isNotEmpty(dictResultList)) { + for (Map dictResult : dictResultList) { + dictResultMap.put(dictResult.get("id").toString(), dictResult.get("name")); + } + } + } else if (dict.getDictType().equals(DictType.URL)) { + this.buildUrlDictDataMap(dict, dictResultMap, true); + } + return dictResultMap; + } + + private List createDefaultFilter(OnlineDict dict) { + List filterList = new LinkedList<>(); + if (StrUtil.isNotBlank(dict.getDeletedColumnName())) { + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(dict.getTableName()); + filter.setColumnName(dict.getDeletedColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + return filterList; + } + + private void buildUrlDictDataMap(OnlineDict dict, Map dictResultMap, boolean keyToValue) { + Map param = new HashMap<>(1); + param.put("Authorization", TokenData.takeFromRequest().getToken()); + String responseData = HttpUtil.get(dict.getDictListUrl(), param); + ResponseResult responseResult = + JSON.parseObject(responseData, new TypeReference>() { + }); + if (!responseResult.isSuccess()) { + throw new OnlineRuntimeException(responseResult.getErrorMessage()); + } + JSONArray dictDataArray = responseResult.getData(); + for (int i = 0; i < dictDataArray.size(); i++) { + JSONObject dictData = dictDataArray.getJSONObject(i); + if (keyToValue) { + dictResultMap.put(dictData.getString(dict.getKeyColumnName()), dictData.get(dict.getValueColumnName())); + } else { + dictResultMap.put(dictData.getString(dict.getValueColumnName()), dictData.get(dict.getKeyColumnName())); + } + } + } + + private void doBindColumnDictData( + List> resultList, + OnlineColumn column, + OnlineDict dict, + Set dictIdDataSet) { + Map dictResultMap = this.doBuildColumnDictDataMap(dict, dictIdDataSet); + String dictKeyName; + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + dictKeyName = column.getColumnAliasName() + DICT_MAP_LIST_SUFFIX; + } else { + dictKeyName = column.getColumnAliasName() + DICT_MAP_SUFFIX; + } + for (Map result : resultList) { + Object dictIdData = result.get(column.getColumnAliasName()); + if (ObjectUtil.isEmpty(dictIdData)) { + continue; + } + if (ObjectUtil.equals(column.getFieldKind(), FieldKind.DICT_MULTI_SELECT)) { + List dictIdDataList = StrUtil.splitTrim(dictIdData.toString(), ","); + List> dictMapList = new LinkedList<>(); + for (String data : dictIdDataList) { + Object dictNameData = dictResultMap.get(data); + Map dictMap = new HashMap<>(2); + dictMap.put("id", data); + dictMap.put("name", dictNameData); + dictMapList.add(dictMap); + } + result.put(dictKeyName, dictMapList); + } else { + Object dictNameData = dictResultMap.get(dictIdData.toString()); + Map dictMap = new HashMap<>(2); + dictMap.put("id", dictIdData); + dictMap.put("name", dictNameData); + result.put(dictKeyName, dictMap); + } + } + } + + private List makeJoinInfoList( + OnlineTable masterTable, List relationList) { + List joinInfoList = new LinkedList<>(); + if (CollUtil.isEmpty(relationList)) { + return joinInfoList; + } + Map masterTableColumnMap = masterTable.getColumnMap(); + for (OnlineDatasourceRelation relation : relationList) { + JoinTableInfo joinInfo = new JoinTableInfo(); + joinInfo.setLeftJoin(relation.getLeftJoin()); + joinInfo.setJoinTableName(relation.getSlaveTable().getTableName() + " " + relation.getVariableName()); + // 根据配置动态拼接JOIN的关联条件,同时要考虑从表的逻辑删除过滤。 + OnlineColumn masterColumn = masterTableColumnMap.get(relation.getMasterColumnId()); + OnlineColumn slaveColumn = relation.getSlaveTable().getColumnMap().get(relation.getSlaveColumnId()); + StringBuilder conditionBuilder = new StringBuilder(64); + conditionBuilder + .append(masterTable.getTableName()) + .append(".") + .append(masterColumn.getColumnName()) + .append(" = ") + .append(relation.getVariableName()) + .append(".") + .append(slaveColumn.getColumnName()); + if (relation.getSlaveTable().getLogicDeleteColumn() != null) { + conditionBuilder + .append(AND) + .append(relation.getVariableName()) + .append(".") + .append(relation.getSlaveTable().getLogicDeleteColumn().getColumnName()) + .append(" = ") + .append(GlobalDeletedFlag.NORMAL); + } + joinInfo.setJoinCondition(conditionBuilder.toString()); + joinInfoList.add(joinInfo); + } + return joinInfoList; + } + + private String makeSelectFields(OnlineTable table, String relationVariable) { + DataSourceProvider provider = dataSourceUtil.getProvider(table.getDblinkId()); + StringBuilder selectFieldBuider = new StringBuilder(512); + String intString = "SIGNED"; + if (provider.getDblinkType() == DblinkType.POSTGRESQL|| provider.getDblinkType() == DblinkType.OPENGAUSS) { + intString = "INT8"; + } + // 拼装主表的select fields字段。 + for (OnlineColumn column : table.getColumnMap().values()) { + OnlineColumn deletedColumn = table.getLogicDeleteColumn(); + String columnAliasName = column.getColumnName(); + if (relationVariable != null) { + columnAliasName = relationVariable + + OnlineConstant.RELATION_TABLE_COLUMN_SEPARATOR + column.getColumnName(); + } + if (deletedColumn != null && StrUtil.equals(column.getColumnName(), deletedColumn.getColumnName())) { + continue; + } + if (this.castToInteger(column)) { + selectFieldBuider + .append("CAST(") + .append(table.getTableName()) + .append(".") + .append(column.getColumnName()) + .append(" AS ") + .append(intString) + .append(") \"") + .append(columnAliasName) + .append("\","); + } else if ("date".equals(column.getColumnType())) { + selectFieldBuider + .append("CAST(") + .append(table.getTableName()) + .append(".") + .append(column.getColumnName()) + .append(" AS CHAR(10)) \"") + .append(columnAliasName) + .append("\","); + } else { + selectFieldBuider + .append(table.getTableName()) + .append(".") + .append(column.getColumnName()) + .append(" \"") + .append(columnAliasName) + .append("\","); + } + } + return selectFieldBuider.substring(0, selectFieldBuider.length() - 1); + } + + private String makeSelectFieldsWithRelation( + OnlineTable masterTable, List relationList) { + String masterTableSelectFields = this.makeSelectFields(masterTable, null); + if (CollUtil.isEmpty(relationList)) { + return masterTableSelectFields; + } + StringBuilder selectFieldBuider = new StringBuilder(512); + selectFieldBuider.append(masterTableSelectFields).append(","); + for (OnlineDatasourceRelation relation : relationList) { + OnlineTable slaveTable = relation.getSlaveTable(); + String relationTableSelectFields = this.makeSelectFields(slaveTable, relation.getVariableName()); + selectFieldBuider.append(relationTableSelectFields).append(","); + } + return selectFieldBuider.substring(0, selectFieldBuider.length() - 1); + } + + private String makeDictSelectFields(OnlineDict onlineDict, boolean ignoreParentId) { + StringBuilder sb = new StringBuilder(128); + sb.append(onlineDict.getKeyColumnName()).append(" \"id\", "); + sb.append(onlineDict.getValueColumnName()).append(" \"name\""); + if (!ignoreParentId && BooleanUtil.isTrue(onlineDict.getTreeFlag())) { + sb.append(", ").append(onlineDict.getParentKeyColumnName()).append(" \"parentId\""); + } + return sb.toString(); + } + + private boolean castToInteger(OnlineColumn column) { + return "tinyint(1)".equals(column.getFullColumnType()); + } + + private String makeColumnNames(List columnDataList) { + StringBuilder sb = new StringBuilder(512); + for (ColumnData columnData : columnDataList) { + if (BooleanUtil.isTrue(columnData.getColumn().getAutoIncrement())) { + continue; + } + sb.append(columnData.getColumn().getColumnName()).append(","); + } + return sb.substring(0, sb.length() - 1); + } + + private void makeupColumnValue(ColumnData columnData) { + if (BooleanUtil.isTrue(columnData.getColumn().getAutoIncrement())) { + return; + } + if (BooleanUtil.isTrue(columnData.getColumn().getPrimaryKey())) { + if (columnData.getColumnValue() == null + && BooleanUtil.isFalse(columnData.getColumn().getAutoIncrement())) { + if (ObjectFieldType.LONG.equals(columnData.getColumn().getObjectFieldType())) { + columnData.setColumnValue(idGenerator.nextLongId()); + } else { + columnData.setColumnValue(idGenerator.nextStringId()); + } + } + } else if (columnData.getColumn().getFieldKind() != null) { + this.makeupColumnValueForFieldKind(columnData); + } else if (columnData.getColumn().getColumnDefault() != null + && columnData.getColumnValue() == null) { + Object v = onlineOperationHelper.convertToTypeValue( + columnData.getColumn(), columnData.getColumn().getColumnDefault()); + columnData.setColumnValue(v); + } + } + + private void makeupColumnValueForFieldKind(ColumnData columnData) { + switch (columnData.getColumn().getFieldKind()) { + case FieldKind.CREATE_TIME: + case FieldKind.UPDATE_TIME: + columnData.setColumnValue(LocalDateTime.now()); + break; + case FieldKind.CREATE_USER_ID: + case FieldKind.UPDATE_USER_ID: + columnData.setColumnValue(TokenData.takeFromRequest().getUserId()); + break; + case FieldKind.CREATE_DEPT_ID: + columnData.setColumnValue(TokenData.takeFromRequest().getDeptId()); + break; + case FieldKind.LOGIC_DELETE: + columnData.setColumnValue(GlobalDeletedFlag.NORMAL); + break; + default: + break; + } + } + + private List makeDefaultFilter(OnlineTable table, OnlineColumn column, String columnValue) { + List filterList = new LinkedList<>(); + OnlineFilterDto dataIdFilter = new OnlineFilterDto(); + dataIdFilter.setTableName(table.getTableName()); + dataIdFilter.setColumnName(column.getColumnName()); + dataIdFilter.setColumnValue(onlineOperationHelper.convertToTypeValue(column, columnValue)); + filterList.add(dataIdFilter); + if (table.getLogicDeleteColumn() != null) { + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(table.getTableName()); + filter.setColumnName(table.getLogicDeleteColumn().getColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + return filterList; + } + + private void doLogicDelete( + OnlineTable table, List filterList, String dataPermFilter) { + List updateColumnList = new LinkedList<>(); + ColumnData logicDeleteColumnData = new ColumnData(); + logicDeleteColumnData.setColumn(table.getLogicDeleteColumn()); + logicDeleteColumnData.setColumnValue(GlobalDeletedFlag.DELETED); + updateColumnList.add(logicDeleteColumnData); + this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + } + + private void doLogicDelete( + OnlineTable table, OnlineColumn filterColumn, String filterColumnValue, String dataPermFilter) { + List filterList = new LinkedList<>(); + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(table.getTableName()); + filter.setColumnName(filterColumn.getColumnName()); + filter.setColumnValue(onlineOperationHelper.convertToTypeValue(filterColumn, filterColumnValue)); + filterList.add(filter); + this.doLogicDelete(table, filterList, dataPermFilter); + } + + private void normalizeFilterList( + OnlineTable table, List oneToOneRelationList, List filterList) { + if (table.getLogicDeleteColumn() != null) { + if (filterList == null) { + filterList = new LinkedList<>(); + } + OnlineFilterDto filter = new OnlineFilterDto(); + filter.setTableName(table.getTableName()); + filter.setColumnName(table.getLogicDeleteColumn().getColumnName()); + filter.setColumnValue(GlobalDeletedFlag.NORMAL); + filterList.add(filter); + } + if (CollUtil.isEmpty(filterList)) { + return; + } + OnlineDblink dblink = onlineDblinkService.getById(table.getDblinkId()); + for (OnlineFilterDto filter : filterList) { + // oracle 日期字段的,后面要重写这段代码,以便具有更好的通用性。 + if (filter.getFilterType().equals(FieldFilterType.RANGE_FILTER)) { + this.makeRangeFilter(dblink, table, oneToOneRelationList, filter); + } + if (BooleanUtil.isTrue(filter.getDictMultiSelect())) { + filter.setFilterType(FieldFilterType.MULTI_LIKE); + List dictValueSet = StrUtil.split(filter.getColumnValue().toString(), ","); + filter.setColumnValueList( + dictValueSet.stream().map(v -> "%" + v + ",%").collect(Collectors.toSet())); + } + if (filter.getFilterType().equals(FieldFilterType.LIKE_FILTER)) { + filter.setColumnValue("%" + filter.getColumnValue() + "%"); + } else if (filter.getFilterType().equals(FieldFilterType.IN_LIST_FILTER) + && ObjectUtil.isNotEmpty(filter.getColumnValue())) { + filter.setColumnValueList( + new HashSet<>(StrUtil.split(filter.getColumnValue().toString(), ","))); + } + } + } + + private String normalizeSlaveTableAlias(List relationList, String s) { + if (CollUtil.isEmpty(relationList) || StrUtil.isBlank(s)) { + return s; + } + for (OnlineDatasourceRelation r : relationList) { + s = StrUtil.replace(s, r.getSlaveTable().getTableName() + ".", r.getVariableName() + "."); + } + return s; + } + + private void normalizeFiltersSlaveTableAlias( + List relationList, List filters) { + if (CollUtil.isEmpty(relationList) || CollUtil.isEmpty(filters)) { + return; + } + for (OnlineDatasourceRelation r : relationList) { + for (OnlineFilterDto filter : filters) { + if (StrUtil.equals(filter.getTableName(), r.getSlaveTable().getTableName())) { + filter.setTableName(r.getVariableName()); + } + } + } + } + + private void makeRangeFilter( + OnlineDblink dblink, + OnlineTable table, + List oneToOneRelationList, + OnlineFilterDto filter) { + if (!dblink.getDblinkType().equals(DblinkType.ORACLE)) { + return; + } + OnlineColumn column = table.getColumnMap().values().stream() + .filter(c -> c.getColumnName().equals(filter.getColumnName())).findFirst().orElse(null); + if (column == null && oneToOneRelationList != null) { + for (OnlineDatasourceRelation r : oneToOneRelationList) { + column = r.getSlaveTable().getColumnMap().values().stream() + .filter(c -> c.getColumnName().equals(filter.getColumnName())).findFirst().orElse(null); + if (column != null) { + break; + } + } + } + org.springframework.util.Assert.notNull(column, "column can't be NULL."); + filter.setIsOracleDate(StrUtil.equals(column.getObjectFieldType(), "Date")); + if (BooleanUtil.isTrue(filter.getIsOracleDate())) { + if (filter.getColumnValueStart() != null) { + filter.setColumnValueStart("TO_DATE('" + filter.getColumnValueStart() + "','YYYY-MM-DD HH24:MI:SS')"); + } + if (filter.getColumnValueEnd() != null) { + filter.setColumnValueEnd("TO_DATE('" + filter.getColumnValueEnd() + "','YYYY-MM-DD HH24:MI:SS')"); + } + } + } + + private String buildDataPermFilter(String tableName, String deptFilterColumnName, String userFilterColumnName) { + if (BooleanUtil.isFalse(dataFilterProperties.getEnabledDataPermFilter())) { + return null; + } + if (!GlobalThreadLocal.enabledDataFilter()) { + return null; + } + return processDataPerm(tableName, deptFilterColumnName, userFilterColumnName); + } + + private String buildDataPermFilter(OnlineTable table) { + if (BooleanUtil.isFalse(dataFilterProperties.getEnabledDataPermFilter())) { + return null; + } + if (!GlobalThreadLocal.enabledDataFilter()) { + return null; + } + String deptFilterColumnName = null; + String userFilterColumnName = null; + for (OnlineColumn column : table.getColumnMap().values()) { + if (BooleanUtil.isTrue(column.getDeptFilter())) { + deptFilterColumnName = column.getColumnName(); + } + if (BooleanUtil.isTrue(column.getUserFilter())) { + userFilterColumnName = column.getColumnName(); + } + } + return processDataPerm(table.getTableName(), deptFilterColumnName, userFilterColumnName); + } + + private String processDataPerm(String tableName, String deptFilterColumnName, String userFilterColumnName) { + TokenData tokenData = TokenData.takeFromRequest(); + if (Boolean.TRUE.equals(tokenData.getIsAdmin())) { + return null; + } + if (StrUtil.isAllBlank(deptFilterColumnName, userFilterColumnName)) { + return null; + } + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + Object cachedData = this.getCachedData(dataPermSessionKey); + if (cachedData == null) { + throw new NoDataPermException("No Related DataPerm found For OnlineForm Module."); + } + JSONObject allMenuDataPermMap = cachedData instanceof JSONObject + ? (JSONObject) cachedData : JSON.parseObject(cachedData.toString()); + JSONObject menuDataPermMap = this.getAndVerifyMenuDataPerm(allMenuDataPermMap, tableName); + Map dataPermMap = new HashMap<>(8); + for (Map.Entry entry : menuDataPermMap.entrySet()) { + dataPermMap.put(Integer.valueOf(entry.getKey()), entry.getValue().toString()); + } + if (MapUtil.isEmpty(dataPermMap)) { + throw new NoDataPermException(StrFormatter.format( + "No Related OnlineForm DataPerm found for table [{}].", tableName)); + } + if (dataPermMap.containsKey(DataPermRuleType.TYPE_ALL)) { + return null; + } + return doProcessDataPerm(tableName, deptFilterColumnName, userFilterColumnName, dataPermMap); + } + + private JSONObject getAndVerifyMenuDataPerm(JSONObject allMenuDataPermMap, String tableName) { + String menuId = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_MENU_ID); + if (menuId == null) { + menuId = ContextUtil.getHttpRequest().getParameter(ApplicationConstant.HTTP_HEADER_MENU_ID); + } + if (BooleanUtil.isFalse(dataFilterProperties.getEnableMenuPermVerify()) && menuId == null) { + menuId = ApplicationConstant.DATA_PERM_ALL_MENU_ID; + } + Assert.notNull(menuId); + JSONObject menuDataPermMap = allMenuDataPermMap.getJSONObject(menuId); + if (menuDataPermMap == null) { + menuDataPermMap = allMenuDataPermMap.getJSONObject(ApplicationConstant.DATA_PERM_ALL_MENU_ID); + } + if (menuDataPermMap == null) { + throw new NoDataPermException(StrFormatter.format( + "No Related OnlineForm DataPerm found for menuId [{}] and table [{}].", + menuId, tableName)); + } + if (BooleanUtil.isTrue(dataFilterProperties.getEnableMenuPermVerify())) { + String url = ContextUtil.getHttpRequest().getHeader(ApplicationConstant.HTTP_HEADER_ORIGINAL_REQUEST_URL); + if (StrUtil.isBlank(url)) { + url = ContextUtil.getHttpRequest().getRequestURI(); + } + Assert.notNull(url); + if (!this.verifyMenuPerm(null, url, tableName) && !this.verifyMenuPerm(menuId, url, tableName)) { + String msg = StrFormatter.format("Mismatched OnlineForm DataPerm " + + "for menuId [{}] and url [{}] and SQL_ID [{}].", menuId, url, tableName); + throw new NoDataPermException(msg); + } + } + return menuDataPermMap; + } + + private Object getCachedData(String dataPermSessionKey) { + Object cachedData = null; + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.DATA_PERMISSION_CACHE.name()); + if (cache == null) { + return cachedData; + } + Cache.ValueWrapper wrapper = cache.get(dataPermSessionKey); + if (wrapper == null) { + cachedData = redissonClient.getBucket(dataPermSessionKey).get(); + if (cachedData != null) { + cache.put(dataPermSessionKey, JSON.parseObject(cachedData.toString())); + } + } else { + cachedData = wrapper.get(); + } + return cachedData; + } + + @SuppressWarnings("unchecked") + private boolean verifyMenuPerm(String menuId, String url, String tableName) { + String sessionId = TokenData.takeFromRequest().getSessionId(); + String menuPermSessionKey; + if (menuId != null) { + menuPermSessionKey = RedisKeyUtil.makeSessionMenuPermKey(sessionId, menuId); + } else { + menuPermSessionKey = RedisKeyUtil.makeSessionWhiteListPermKey(sessionId); + } + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.MENU_PERM_CACHE.name()); + if (cache == null) { + return false; + } + Cache.ValueWrapper wrapper = cache.get(menuPermSessionKey); + if (wrapper != null) { + Object cacheData = wrapper.get(); + if (cacheData != null) { + return ((Set) cacheData).contains(url); + } + } + RBucket bucket = redissonClient.getBucket(menuPermSessionKey); + if (!bucket.isExists()) { + String msg; + if (menuId == null) { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for WHITE_LIST and tableName [{}] with sessionId [{}].", tableName, sessionId); + } else { + msg = StrFormatter.format("No Related MenuPerm found " + + "in Redis Cache for menuId [{}] and tableName[{}] with sessionId [{}].", menuId, tableName, sessionId); + } + throw new NoDataPermException(msg); + } + Set cachedMenuPermSet = new HashSet<>(JSONArray.parseArray(bucket.get(), String.class)); + cache.put(menuPermSessionKey, cachedMenuPermSet); + return cachedMenuPermSet.contains(url); + } + + private String doProcessDataPerm( + String tableName, String deptFilterColumnName, String userFilterColumnName, Map dataPermMap) { + List criteriaList = new LinkedList<>(); + for (Map.Entry entry : dataPermMap.entrySet()) { + String filterClause = processDataPermRule( + tableName, deptFilterColumnName, userFilterColumnName, entry.getKey(), entry.getValue()); + if (StrUtil.isNotBlank(filterClause)) { + criteriaList.add(filterClause); + } + } + if (CollUtil.isEmpty(criteriaList)) { + return null; + } + StringBuilder filterBuilder = new StringBuilder(128); + filterBuilder.append("("); + filterBuilder.append(CollUtil.join(criteriaList, " OR ")); + filterBuilder.append(")"); + return filterBuilder.toString(); + } + + private String processDataPermRule( + String tableName, String deptFilterColumnName, String userFilterColumnName, Integer ruleType, String dataIds) { + TokenData tokenData = TokenData.takeFromRequest(); + StringBuilder filter = new StringBuilder(128); + if (ruleType != DataPermRuleType.TYPE_USER_ONLY + && ruleType != DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS + && ruleType != DataPermRuleType.TYPE_DEPT_USERS) { + return this.processDeptDataPermRule(tableName, deptFilterColumnName, ruleType, dataIds); + } + if (StrUtil.isBlank(userFilterColumnName)) { + log.warn("No UserFilterColumn for ONLINE table [{}] but USER_FILTER_DATA_PERM exists", tableName); + return filter.toString(); + } + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + if (ruleType == DataPermRuleType.TYPE_USER_ONLY) { + filter.append(userFilterColumnName).append(" = ").append(tokenData.getUserId()); + } else { + filter.append(userFilterColumnName) + .append(" IN (") + .append(dataIds) + .append(") "); + } + return filter.toString(); + } + + private String processDeptDataPermRule( + String tableName, String deptFilterColumnName, Integer ruleType, String deptIds) { + TokenData tokenData = TokenData.takeFromRequest(); + StringBuilder filter = new StringBuilder(256); + if (StrUtil.isBlank(deptFilterColumnName)) { + log.warn("No DeptFilterColumn for ONLINE table [{}] but DEPT_FILTER_DATA_PERM exists", tableName); + return filter.toString(); + } + if (ruleType == DataPermRuleType.TYPE_DEPT_ONLY) { + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName).append(" = ").append(tokenData.getDeptId()); + } else if (ruleType == DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id = ") + .append(tokenData.getDeptId()) + .append(AND); + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName) + .append(" = ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT) { + filter.append(" EXISTS ") + .append("(SELECT 1 FROM ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation WHERE ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.parent_dept_id IN (") + .append(deptIds) + .append(") AND "); + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName) + .append(" = ") + .append(dataFilterProperties.getDeptRelationTablePrefix()) + .append("sys_dept_relation.dept_id) "); + } else if (ruleType == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) { + if (BooleanUtil.isTrue(dataFilterProperties.getAddTableNamePrefix())) { + filter.append(tableName).append("."); + } + filter.append(deptFilterColumnName).append(" IN (").append(deptIds).append(") "); + } + return filter.toString(); + } + + @Data + private static class VirtualColumnWhereClause { + private Long tableId; + private Long columnId; + private Integer operatorType; + private Object value; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java new file mode 100644 index 00000000..f130bf17 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlinePageServiceImpl.java @@ -0,0 +1,299 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlinePageDatasourceMapper; +import com.orangeforms.common.online.dao.OnlinePageMapper; +import com.orangeforms.common.online.model.OnlinePage; +import com.orangeforms.common.online.model.OnlinePageDatasource; +import com.orangeforms.common.online.model.constant.PageStatus; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineFormService; +import com.orangeforms.common.online.service.OnlinePageService; +import com.github.pagehelper.Page; +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.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * 在线表单页面数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlinePageService") +public class OnlinePageServiceImpl extends BaseService implements OnlinePageService { + + @Autowired + private OnlinePageMapper onlinePageMapper; + @Autowired + private OnlinePageDatasourceMapper onlinePageDatasourceMapper; + @Autowired + private OnlineFormService onlineFormService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlinePageMapper; + } + + /** + * 保存新增对象。 + * + * @param onlinePage 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlinePage saveNew(OnlinePage onlinePage) { + TokenData tokenData = TokenData.takeFromRequest(); + onlinePage.setPageId(idGenerator.nextLongId()); + onlinePage.setAppCode(tokenData.getAppCode()); + onlinePage.setTenantId(tokenData.getTenantId()); + Date now = new Date(); + onlinePage.setUpdateTime(now); + onlinePage.setCreateTime(now); + onlinePage.setCreateUserId(tokenData.getUserId()); + onlinePage.setUpdateUserId(tokenData.getUserId()); + onlinePage.setPublished(false); + MyModelUtil.setDefaultValue(onlinePage, "status", PageStatus.BASIC); + onlinePageMapper.insert(onlinePage); + return onlinePage; + } + + /** + * 更新数据对象。 + * + * @param onlinePage 更新的对象。 + * @param originalOnlinePage 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlinePage onlinePage, OnlinePage originalOnlinePage) { + TokenData tokenData = TokenData.takeFromRequest(); + onlinePage.setAppCode(tokenData.getAppCode()); + onlinePage.setTenantId(tokenData.getTenantId()); + onlinePage.setUpdateTime(new Date()); + onlinePage.setUpdateUserId(tokenData.getUserId()); + onlinePage.setCreateTime(originalOnlinePage.getCreateTime()); + onlinePage.setCreateUserId(originalOnlinePage.getCreateUserId()); + onlinePage.setPublished(originalOnlinePage.getPublished()); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(onlinePage, onlinePage.getPageId()); + return onlinePageMapper.update(onlinePage, uw) == 1; + } + + /** + * 更新页面对象的发布状态。 + * + * @param pageId 页面对象Id。 + * @param published 新的状态。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void updatePublished(Long pageId, Boolean published) { + OnlinePage onlinePage = new OnlinePage(); + onlinePage.setPageId(pageId); + onlinePage.setPublished(published); + onlinePage.setUpdateTime(new Date()); + onlinePage.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + onlinePageMapper.updateById(onlinePage); + } + + /** + * 删除指定数据,及其包含的表单和数据源等。 + * + * @param pageId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long pageId) { + if (onlinePageMapper.deleteById(pageId) == 0) { + return false; + } + // 开始删除关联表单。 + onlineFormService.removeByPageId(pageId); + // 先获取出关联的表单和数据源。 + OnlinePageDatasource pageDatasourceFilter = new OnlinePageDatasource(); + pageDatasourceFilter.setPageId(pageId); + List pageDatasourceList = + onlinePageDatasourceMapper.selectList(new QueryWrapper<>(pageDatasourceFilter)); + if (CollUtil.isNotEmpty(pageDatasourceList)) { + for (OnlinePageDatasource pageDatasource : pageDatasourceList) { + onlineDatasourceService.remove(pageDatasource.getDatasourceId()); + } + } + return true; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlinePageListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlinePageList(OnlinePage filter, String orderBy) { + if (filter == null) { + filter = new OnlinePage(); + } + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlinePageMapper.getOnlinePageList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlinePageList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlinePageListWithRelation(OnlinePage filter, String orderBy) { + List resultList = this.getOnlinePageList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 批量添加多对多关联关系。 + * + * @param onlinePageDatasourceList 多对多关联表对象集合。 + * @param pageId 主表Id。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void addOnlinePageDatasourceList(List onlinePageDatasourceList, Long pageId) { + for (OnlinePageDatasource onlinePageDatasource : onlinePageDatasourceList) { + onlinePageDatasource.setPageId(pageId); + onlinePageDatasourceMapper.insert(onlinePageDatasource); + } + } + + /** + * 获取中间表数据。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 中间表对象。 + */ + @Override + public OnlinePageDatasource getOnlinePageDatasource(Long pageId, Long datasourceId) { + OnlinePageDatasource filter = new OnlinePageDatasource(); + filter.setPageId(pageId); + filter.setDatasourceId(datasourceId); + return onlinePageDatasourceMapper.selectOne(new QueryWrapper<>(filter)); + } + + @Override + public List getOnlinePageDatasourceListByPageId(Long pageId) { + OnlinePageDatasource filter = new OnlinePageDatasource(); + filter.setPageId(pageId); + return onlinePageDatasourceMapper.selectList(new QueryWrapper<>(filter)); + } + + /** + * 根据数据源Id,返回使用该数据源的OnlinePage对象。 + * + * @param datasourceId 数据源Id。 + * @return 使用该数据源的页面列表。 + */ + @Override + public List getOnlinePageListByDatasourceId(Long datasourceId) { + OnlinePage filter = new OnlinePage(); + TokenData tokenData = TokenData.takeFromRequest(); + filter.setTenantId(tokenData.getTenantId()); + filter.setAppCode(tokenData.getAppCode()); + return onlinePageMapper.getOnlinePageListByDatasourceId(datasourceId, filter); + } + + /** + * 移除单条多对多关系。 + * + * @param pageId 主表Id。 + * @param datasourceId 从表Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean removeOnlinePageDatasource(Long pageId, Long datasourceId) { + OnlinePageDatasource filter = new OnlinePageDatasource(); + filter.setPageId(pageId); + filter.setDatasourceId(datasourceId); + return onlinePageDatasourceMapper.delete(new QueryWrapper<>(filter)) > 0; + } + + @Override + public boolean existByPageCode(String pageCode) { + OnlinePage filter = new OnlinePage(); + filter.setPageCode(pageCode); + return CollUtil.isNotEmpty(this.getOnlinePageList(filter, null)); + } + + @Override + public List getNotInListWithNonTenant(List pageIds, String orderBy) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (CollUtil.isNotEmpty(pageIds)) { + queryWrapper.notIn(OnlinePage::getPageId, pageIds); + } + queryWrapper.isNull(OnlinePage::getTenantId); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } + return onlinePageMapper.selectList(queryWrapper); + } + + @Override + public List getInListWithNonTenant(List pageIds, String orderBy) { + if (CollUtil.isEmpty(pageIds)) { + return new LinkedList<>(); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(OnlinePage::getPageId, pageIds); + queryWrapper.isNull(OnlinePage::getTenantId); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(" ORDER BY " + orderBy); + } + return onlinePageMapper.selectList(queryWrapper); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java new file mode 100644 index 00000000..64df1a31 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineRuleServiceImpl.java @@ -0,0 +1,248 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.GlobalDeletedFlag; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.MyModelUtil; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.redis.util.CommonRedisUtil; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineColumnRuleMapper; +import com.orangeforms.common.online.dao.OnlineRuleMapper; +import com.orangeforms.common.online.model.OnlineColumnRule; +import com.orangeforms.common.online.model.OnlineRule; +import com.orangeforms.common.online.service.OnlineRuleService; +import com.github.pagehelper.Page; +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.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 验证规则数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineRuleService") +public class OnlineRuleServiceImpl extends BaseService implements OnlineRuleService { + + @Autowired + private OnlineRuleMapper onlineRuleMapper; + @Autowired + private OnlineColumnRuleMapper onlineColumnRuleMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private CommonRedisUtil commonRedisUtil; + @Autowired + private RedissonClient redissonClient; + + /** + * 所有字段规则使用同一个键。 + */ + private static final String ONLINE_RULE_CACHE_KEY = "ONLINE_RULE"; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineRuleMapper; + } + + /** + * 保存新增对象。 + * + * @param onlineRule 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineRule saveNew(OnlineRule onlineRule) { + commonRedisUtil.evictFormCache(ONLINE_RULE_CACHE_KEY); + TokenData tokenData = TokenData.takeFromRequest(); + onlineRule.setRuleId(idGenerator.nextLongId()); + onlineRule.setAppCode(tokenData.getAppCode()); + Date now = new Date(); + onlineRule.setUpdateTime(now); + onlineRule.setCreateTime(now); + onlineRule.setCreateUserId(tokenData.getUserId()); + onlineRule.setUpdateUserId(tokenData.getUserId()); + onlineRule.setBuiltin(false); + onlineRule.setDeletedFlag(GlobalDeletedFlag.NORMAL); + MyModelUtil.setDefaultValue(onlineRule, "pattern", ""); + onlineRuleMapper.insert(onlineRule); + return onlineRule; + } + + /** + * 更新数据对象。 + * + * @param onlineRule 更新的对象。 + * @param originalOnlineRule 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineRule onlineRule, OnlineRule originalOnlineRule) { + commonRedisUtil.evictFormCache(ONLINE_RULE_CACHE_KEY); + TokenData tokenData = TokenData.takeFromRequest(); + onlineRule.setAppCode(tokenData.getAppCode()); + onlineRule.setUpdateTime(new Date()); + onlineRule.setUpdateUserId(tokenData.getUserId()); + onlineRule.setCreateTime(originalOnlineRule.getCreateTime()); + onlineRule.setCreateUserId(originalOnlineRule.getCreateUserId()); + UpdateWrapper uw = this.createUpdateQueryForNullValue(onlineRule, onlineRule.getRuleId()); + return onlineRuleMapper.update(onlineRule, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param ruleId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long ruleId) { + commonRedisUtil.evictFormCache(ONLINE_RULE_CACHE_KEY); + if (onlineRuleMapper.deleteById(ruleId) == 0) { + return false; + } + // 开始删除多对多父表的关联 + OnlineColumnRule onlineColumnRule = new OnlineColumnRule(); + onlineColumnRule.setRuleId(ruleId); + onlineColumnRuleMapper.delete(new QueryWrapper<>(onlineColumnRule)); + return true; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineRuleListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineRuleList(OnlineRule filter, String orderBy) { + if (filter == null) { + filter = new OnlineRule(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + return onlineRuleMapper.getOnlineRuleList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineRuleList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineRuleListWithRelation(OnlineRule filter, String orderBy) { + List resultList = this.getOnlineRuleList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getNotInOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy) { + if (filter == null) { + filter = new OnlineRule(); + } + filter.setAppCode(TokenData.takeFromRequest().getAppCode()); + List resultList = + onlineRuleMapper.getNotInOnlineRuleListByColumnId(columnId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + /** + * 在多对多关系中,当前Service的数据表为从表,返回与指定主表主键Id存在对多对关系的列表。 + * + * @param columnId 主表主键Id。 + * @param filter 从表的过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineRuleListByColumnId(Long columnId, OnlineRule filter, String orderBy) { + List resultList = + onlineRuleMapper.getOnlineRuleListByColumnId(columnId, filter, orderBy); + this.buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + return resultList; + } + + /** + * 返回指定字段Id列表关联的字段规则对象列表。 + * + * @param columnIdSet 指定的字段Id列表。 + * @return 关联的字段规则对象列表。 + */ + @Override + public List getOnlineColumnRuleListByColumnIds(Set columnIdSet) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(OnlineColumnRule::getColumnId, columnIdSet); + List columnRuleList = onlineColumnRuleMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(columnRuleList)) { + return columnRuleList; + } + List ruleList; + RBucket bucket = redissonClient.getBucket(ONLINE_RULE_CACHE_KEY); + if (bucket.isExists()) { + ruleList = JSONArray.parseArray(bucket.get(), OnlineRule.class); + } else { + ruleList = this.getAllList(); + if (CollUtil.isNotEmpty(ruleList)) { + bucket.set(JSONArray.toJSONString(ruleList)); + } + } + if (CollUtil.isEmpty(ruleList)) { + return columnRuleList; + } + Map ruleMap = ruleList.stream().collect(Collectors.toMap(OnlineRule::getRuleId, c -> c)); + for (OnlineColumnRule columnRule : columnRuleList) { + columnRule.setOnlineRule(ruleMap.get(columnRule.getRuleId())); + } + return columnRuleList; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java new file mode 100644 index 00000000..ea2cda24 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineTableServiceImpl.java @@ -0,0 +1,195 @@ +package com.orangeforms.common.online.service.impl; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.dbutil.object.SqlTable; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.orangeforms.common.online.dao.OnlineTableMapper; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.online.util.OnlineRedisKeyUtil; +import com.google.common.base.CaseFormat; +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.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 数据表数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineTableService") +public class OnlineTableServiceImpl extends BaseService implements OnlineTableService { + + @Autowired + private OnlineTableMapper onlineTableMapper; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private IdGeneratorWrapper idGenerator; + @Autowired + private RedissonClient redissonClient; + + /** + * 在线对象表的缺省缓存时间(小时)。 + */ + private static final int DEFAULT_CACHED_TABLE_HOURS = 168; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineTableMapper; + } + + /** + * 基于数据库表保存新增对象。 + * + * @param sqlTable 数据库表对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineTable saveNewFromSqlTable(SqlTable sqlTable) { + OnlineTable onlineTable = new OnlineTable(); + TokenData tokenData = TokenData.takeFromRequest(); + onlineTable.setAppCode(tokenData.getAppCode()); + onlineTable.setDblinkId(sqlTable.getDblinkId()); + onlineTable.setTableId(idGenerator.nextLongId()); + onlineTable.setTableName(sqlTable.getTableName()); + String modelName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, sqlTable.getTableName()); + onlineTable.setModelName(modelName); + Date now = new Date(); + onlineTable.setUpdateTime(now); + onlineTable.setCreateTime(now); + onlineTable.setCreateUserId(tokenData.getUserId()); + onlineTable.setUpdateUserId(tokenData.getUserId()); + onlineTableMapper.insert(onlineTable); + List columnList = onlineColumnService.saveNewList(sqlTable.getColumnList(), onlineTable.getTableId()); + onlineTable.setColumnList(columnList); + return onlineTable; + } + + /** + * 删除指定表及其关联的字段数据。 + * + * @param tableId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long tableId) { + if (onlineTableMapper.deleteById(tableId) == 0) { + return false; + } + this.evictTableCache(tableId); + onlineColumnService.removeByTableId(tableId); + return true; + } + + /** + * 删除指定数据表Id集合中的表,及其关联字段。 + * + * @param tableIdSet 待删除的数据表Id集合。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByTableIdSet(Set tableIdSet) { + tableIdSet.forEach(this::evictTableCache); + onlineTableMapper.delete( + new QueryWrapper().lambda().in(OnlineTable::getTableId, tableIdSet)); + onlineColumnService.removeByTableIdSet(tableIdSet); + } + + /** + * 根据数据源Id,获取该数据源及其关联所引用的数据表列表。 + * + * @param datasourceId 指定的数据源Id。 + * @return 该数据源及其关联所引用的数据表列表。 + */ + @Override + public List getOnlineTableListByDatasourceId(Long datasourceId) { + return onlineTableMapper.getOnlineTableListByDatasourceId(datasourceId); + } + + /** + * 从缓存中获取指定的表数据及其关联字段列表。优先从缓存中读取,如果不存在则从数据库中读取,并同步到缓存。 + * 该接口方法仅仅用户在线表单的动态数据操作接口,而非在线表单的配置接口。 + * + * @param tableId 表主键Id。 + * @return 查询后的在线表对象。 + */ + @Override + public OnlineTable getOnlineTableFromCache(Long tableId) { + String redisKey = OnlineRedisKeyUtil.makeOnlineTableKey(tableId); + RBucket tableBucket = redissonClient.getBucket(redisKey); + if (tableBucket.isExists()) { + String tableInfo = tableBucket.get(); + return JSON.parseObject(tableInfo, OnlineTable.class); + } + OnlineTable table = this.getByIdWithRelation(tableId, MyRelationParam.full()); + if (table == null) { + return null; + } + for (OnlineColumn column : table.getColumnList()) { + if (BooleanUtil.isTrue(column.getPrimaryKey())) { + table.setPrimaryKeyColumn(column); + continue; + } + if (ObjectUtil.equal(column.getFieldKind(), FieldKind.LOGIC_DELETE)) { + table.setLogicDeleteColumn(column); + } + } + Map columnMap = + table.getColumnList().stream().collect(Collectors.toMap(OnlineColumn::getColumnId, c -> c)); + table.setColumnMap(columnMap); + table.setColumnList(null); + tableBucket.set(JSON.toJSONString(table)); + tableBucket.expire(DEFAULT_CACHED_TABLE_HOURS, TimeUnit.HOURS); + return table; + } + + @Override + public OnlineColumn getOnlineColumnFromCache(Long tableId, Long columnId) { + OnlineTable table = this.getOnlineTableFromCache(tableId); + if (table == null) { + return null; + } + return table.getColumnMap().get(columnId); + } + + private void evictTableCache(Long tableId) { + String tableIdKey = OnlineRedisKeyUtil.makeOnlineTableKey(tableId); + redissonClient.getBucket(tableIdKey).delete(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java new file mode 100644 index 00000000..60d272d3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/service/impl/OnlineVirtualColumnServiceImpl.java @@ -0,0 +1,180 @@ +package com.orangeforms.common.online.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.orangeforms.common.core.annotation.MyDataSourceResolver; +import com.orangeforms.common.core.base.dao.BaseDaoMapper; +import com.orangeforms.common.core.base.service.BaseService; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.object.CallResult; +import com.orangeforms.common.core.object.MyRelationParam; +import com.orangeforms.common.core.util.DefaultDataSourceResolver; +import com.orangeforms.common.online.dao.OnlineVirtualColumnMapper; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineVirtualColumn; +import com.orangeforms.common.online.model.constant.VirtualType; +import com.orangeforms.common.online.service.OnlineColumnService; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineVirtualColumnService; +import com.orangeforms.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 虚拟字段数据操作服务类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineVirtualColumnService") +public class OnlineVirtualColumnServiceImpl + extends BaseService implements OnlineVirtualColumnService { + + @Autowired + private OnlineVirtualColumnMapper onlineVirtualColumnMapper; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return onlineVirtualColumnMapper; + } + + /** + * 保存新增对象。 + * + * @param virtualColumn 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public OnlineVirtualColumn saveNew(OnlineVirtualColumn virtualColumn) { + virtualColumn.setVirtualColumnId(idGenerator.nextLongId()); + if (virtualColumn.getVirtualType().equals(VirtualType.AGGREGATION)) { + OnlineDatasource datasource = onlineDatasourceService.getById(virtualColumn.getDatasourceId()); + virtualColumn.setTableId(datasource.getMasterTableId()); + } + onlineVirtualColumnMapper.insert(virtualColumn); + return virtualColumn; + } + + /** + * 更新数据对象。 + * + * @param virtualColumn 更新的对象。 + * @param originalVirtualColumn 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(OnlineVirtualColumn virtualColumn, OnlineVirtualColumn originalVirtualColumn) { + if (virtualColumn.getVirtualType().equals(VirtualType.AGGREGATION) + && !virtualColumn.getDatasourceId().equals(originalVirtualColumn.getDatasourceId())) { + OnlineDatasource datasource = onlineDatasourceService.getById(virtualColumn.getDatasourceId()); + virtualColumn.setTableId(datasource.getMasterTableId()); + } + UpdateWrapper uw = + this.createUpdateQueryForNullValue(virtualColumn, virtualColumn.getVirtualColumnId()); + return onlineVirtualColumnMapper.update(virtualColumn, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param virtualColumnId 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long virtualColumnId) { + return onlineVirtualColumnMapper.deleteById(virtualColumnId) == 1; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getOnlineVirtualColumnListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineVirtualColumnList(OnlineVirtualColumn filter, String orderBy) { + return onlineVirtualColumnMapper.getOnlineVirtualColumnList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getOnlineVirtualColumnList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getOnlineVirtualColumnListWithRelation(OnlineVirtualColumn filter, String orderBy) { + List resultList = onlineVirtualColumnMapper.getOnlineVirtualColumnList(filter, orderBy); + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 根据数据表的集合,查询关联的虚拟字段数据列表。 + * @param tableIdSet 在线数据表Id集合。 + * @return 关联的虚拟字段数据列表。 + */ + @Override + public List getOnlineVirtualColumnListByTableIds(Set tableIdSet) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(OnlineVirtualColumn::getTableId, tableIdSet); + return onlineVirtualColumnMapper.selectList(queryWrapper); + } + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param virtualColumn 最新数据对象。 + * @param originalVirtualColumn 原有数据对象。 + * @return 数据全部正确返回true,否则false。 + */ + @Override + public CallResult verifyRelatedData(OnlineVirtualColumn virtualColumn, OnlineVirtualColumn originalVirtualColumn) { + String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!"; + if (this.needToVerify(virtualColumn, originalVirtualColumn, OnlineVirtualColumn::getDatasourceId) + && !onlineDatasourceService.existId(virtualColumn.getDatasourceId())) { + return CallResult.error(String.format(errorMessageFormat, "数据源Id")); + } + if (this.needToVerify(virtualColumn, originalVirtualColumn, OnlineVirtualColumn::getRelationId) + && !onlineDatasourceRelationService.existId(virtualColumn.getRelationId())) { + return CallResult.error(String.format(errorMessageFormat, "数据源关联Id")); + } + if (this.needToVerify(virtualColumn, originalVirtualColumn, OnlineVirtualColumn::getAggregationColumnId) + && !onlineColumnService.existId(virtualColumn.getAggregationColumnId())) { + return CallResult.error(String.format(errorMessageFormat, "聚合字段Id")); + } + return CallResult.ok(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java new file mode 100644 index 00000000..f40866dc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineConstant.java @@ -0,0 +1,21 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单使用的常量数据。。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineConstant { + + /** + * 数据源关联变量名和从表字段名之间的连接字符串。 + */ + public static final String RELATION_TABLE_COLUMN_SEPARATOR = "__"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private OnlineConstant() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java new file mode 100644 index 00000000..a46868b3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomExtFactory.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.util; + +import org.springframework.stereotype.Component; + +/** + * 在线表单自定义扩展工厂类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class OnlineCustomExtFactory { + + private OnlineCustomMaskFieldHandler customMaskFieldHandler = new OnlineCustomMaskFieldHandler(); + + /** + * 设置自定义脱敏规则处理器对象。推荐设置的对象为Bean对象,并在服务启动过程中完成自动注册,运行时直接使用即可。 + * + * @param customMaskFieldHandler 自定义脱敏规则处理器对象。 + */ + public void setCustomMaskFieldHandler(OnlineCustomMaskFieldHandler customMaskFieldHandler) { + this.customMaskFieldHandler = customMaskFieldHandler; + } + + /** + * 返回在线表单的自定义脱敏规则处理器对象。该Bean对象需要在业务代码中实现自行实现。 + * + * @return 在线表单的自定义脱敏规则处理器对象。 + */ + public OnlineCustomMaskFieldHandler getCustomMaskFieldHandler() { + return customMaskFieldHandler; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java new file mode 100644 index 00000000..e99b0e58 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineCustomMaskFieldHandler.java @@ -0,0 +1,25 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单自定义脱敏处理器的默认实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineCustomMaskFieldHandler { + + /** + * 处理自定义的脱敏数据。可以根据表名和字段名,使用不同的自定义脱敏规则。 + * + * @param appCode 应用编码。如果不是第三方接入的应用,该值可能为null。 + * @param tableName 在线表单对应的表名。 + * @param columnName 在线表单对应的表字段名 + * @param data 待脱敏的数据。 + * @param maskChar 脱敏掩码字符。 + * @return 脱敏后的数据。 + */ + public String handleMask(String appCode, String tableName, String columnName, String data, char maskChar) { + throw new UnsupportedOperationException( + "在运行时抛出该异常,主要为了及时提醒用户提供自己的处理器实现类。请在业务工程中提供该类的具体实现类!"); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java new file mode 100644 index 00000000..a4b765a9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineDataSourceUtil.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.online.util; + +import com.orangeforms.common.core.exception.MyRuntimeException; +import com.orangeforms.common.dbutil.provider.DataSourceProvider; +import com.orangeforms.common.dbutil.util.DataSourceUtil; +import com.orangeforms.common.online.model.OnlineDblink; +import com.orangeforms.common.online.service.OnlineDblinkService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 在线表单模块动态加载的数据源工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class OnlineDataSourceUtil extends DataSourceUtil { + + @Autowired + private OnlineDblinkService dblinkService; + + @Override + protected int getDblinkTypeByDblinkId(Long dblinkId) { + DataSourceProvider provider = this.dblinkProviderMap.get(dblinkId); + if (provider != null) { + return provider.getDblinkType(); + } + OnlineDblink dblink = dblinkService.getById(dblinkId); + if (dblink == null) { + throw new MyRuntimeException("Online DblinkId [" + dblinkId + "] doesn't exist!"); + } + this.dblinkProviderMap.put(dblinkId, this.getProvider(dblink.getDblinkType())); + return dblink.getDblinkType(); + } + + @Override + protected String getDblinkConfigurationByDblinkId(Long dblinkId) { + OnlineDblink dblink = dblinkService.getById(dblinkId); + if (dblink == null) { + throw new MyRuntimeException("Online DblinkId [" + dblinkId + "] doesn't exist!"); + } + return dblink.getConfiguration(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java new file mode 100644 index 00000000..c0013375 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineOperationHelper.java @@ -0,0 +1,419 @@ +package com.orangeforms.common.online.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.constant.ObjectFieldType; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.upload.BaseUpDownloader; +import com.orangeforms.common.core.upload.UpDownloaderFactory; +import com.orangeforms.common.core.upload.UploadResponseInfo; +import com.orangeforms.common.core.upload.UploadStoreTypeEnum; +import com.orangeforms.common.online.config.OnlineProperties; +import com.orangeforms.common.online.model.OnlineColumn; +import com.orangeforms.common.online.model.OnlineDatasource; +import com.orangeforms.common.online.model.OnlineDatasourceRelation; +import com.orangeforms.common.online.model.OnlineTable; +import com.orangeforms.common.online.model.constant.FieldKind; +import com.orangeforms.common.online.model.constant.RelationType; +import com.orangeforms.common.online.object.ColumnData; +import com.orangeforms.common.online.service.OnlineDatasourceRelationService; +import com.orangeforms.common.online.service.OnlineDatasourceService; +import com.orangeforms.common.online.service.OnlineOperationService; +import com.orangeforms.common.online.service.OnlineTableService; +import com.orangeforms.common.redis.cache.SessionCacheHelper; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线表单操作的通用帮助对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class OnlineOperationHelper { + + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDatasourceRelationService onlineDatasourceRelationService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineOperationService onlineOperationService; + @Autowired + private OnlineProperties onlineProperties; + @Autowired + private UpDownloaderFactory upDownloaderFactory; + @Autowired + private SessionCacheHelper cacheHelper; + + /** + * 验证并获取数据源数据。 + * + * @param datasourceId 数据源Id。 + * @return 数据源详情数据。 + */ + public ResponseResult verifyAndGetDatasource(Long datasourceId) { + String errorMessage; + OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceFromCache(datasourceId); + if (datasource == null) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + if (!StrUtil.equals(datasource.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该数据源Id"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineTable masterTable = onlineTableService.getOnlineTableFromCache(datasource.getMasterTableId()); + if (masterTable == null) { + errorMessage = "数据验证失败,数据源主表Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + datasource.setMasterTable(masterTable); + return ResponseResult.success(datasource); + } + + /** + * 验证并获取数据源的关联数据。 + * + * @param datasourceId 数据源Id。 + * @param relationId 数据源关联Id。 + * @return 数据源的关联详情数据。 + */ + public ResponseResult verifyAndGetRelation(Long datasourceId, Long relationId) { + String errorMessage; + OnlineDatasourceRelation relation = + onlineDatasourceRelationService.getOnlineDatasourceRelationFromCache(datasourceId, relationId); + if (relation == null || !relation.getDatasourceId().equals(datasourceId)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + if (!StrUtil.equals(relation.getAppCode(), TokenData.takeFromRequest().getAppCode())) { + errorMessage = "数据验证失败,当前应用不包含该数据源关联Id!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + OnlineTable slaveTable = onlineTableService.getOnlineTableFromCache(relation.getSlaveTableId()); + if (slaveTable == null) { + errorMessage = "数据验证失败,数据源关联 [" + relation.getRelationName() + " ] 引用的从表不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + relation.setSlaveTable(slaveTable); + relation.setSlaveColumn(slaveTable.getColumnMap().get(relation.getSlaveColumnId())); + return ResponseResult.success(relation); + } + + /** + * 验证并获取数据源的指定类型关联数据。 + * + * @param datasourceId 数据源Id。 + * @param relationType 数据源关联类型。 + * @return 数据源指定关联类型的关联数据详情列表。 + */ + public ResponseResult> verifyAndGetRelationList( + Long datasourceId, Integer relationType) { + String errorMessage; + List relationList = onlineDatasourceRelationService + .getOnlineDatasourceRelationListFromCache(CollUtil.newHashSet(datasourceId)); + if (relationType != null) { + relationList = relationList.stream() + .filter(r -> r.getRelationType().equals(relationType)).collect(Collectors.toList()); + } + for (OnlineDatasourceRelation relation : relationList) { + OnlineTable slaveTable = onlineTableService.getOnlineTableFromCache(relation.getSlaveTableId()); + if (slaveTable == null) { + errorMessage = "数据验证失败,数据源关联 [" + relation.getRelationName() + "] 的从表Id不存在!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + relation.setSlaveTable(slaveTable); + } + return ResponseResult.success(relationList); + } + + /** + * 构建在线表的数据记录。 + * + * @param table 在线数据表对象。 + * @param tableData 在线数据表数据。 + * @param forUpdate 是否为更新。 + * @param ignoreSetColumnId 忽略设置的字段Id。 + * @return 在线表的数据记录。 + */ + public ResponseResult> buildTableData( + OnlineTable table, JSONObject tableData, boolean forUpdate, Long ignoreSetColumnId) { + List columnDataList = new LinkedList<>(); + String errorMessage; + for (OnlineColumn column : table.getColumnMap().values()) { + // 判断一下是否为需要自动填入的字段,如果是,这里就都暂时给空值了,后续操作会自动填补。 + // 这里还能避免一次基于tableData的查询,能快几纳秒也是好的。 + if (this.isAutoSettingField(column) || ObjectUtil.equal(column.getColumnId(), ignoreSetColumnId)) { + columnDataList.add(new ColumnData(column, null)); + continue; + } + Object value = this.getColumnValue(tableData, column); + // 对于主键数据的处理。 + if (BooleanUtil.isTrue(column.getPrimaryKey())) { + // 如果是更新则必须包含主键参数。 + if (forUpdate && value == null) { + errorMessage = "数据验证失败,数据表 [" + + table.getTableName() + "] 主键字段 [" + column.getColumnName() + "] 不能为空值!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } else { + if (value == null && !column.getNullable() && StrUtil.isBlank(column.getEncodedRule())) { + errorMessage = "数据验证失败,数据表 [" + + table.getTableName() + "] 字段 [" + column.getColumnName() + "] 不能为空值!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } + columnDataList.add(new ColumnData(column, value)); + } + return ResponseResult.success(columnDataList); + } + + /** + * 构建多个一对多从表的数据列表。 + * + * @param datasourceId 数据源Id。 + * @param slaveData 多个一对多从表数据的JSON对象。 + * @return 构建后的多个一对多从表数据列表。 + */ + public ResponseResult>> buildSlaveDataList( + Long datasourceId, JSONObject slaveData) { + if (slaveData == null) { + return ResponseResult.success(null); + } + Map> relationDataMap = new HashMap<>(slaveData.size()); + for (String key : slaveData.keySet()) { + Long relationId = Long.parseLong(key); + ResponseResult relationResult = this.verifyAndGetRelation(datasourceId, relationId); + if (!relationResult.isSuccess()) { + return ResponseResult.errorFrom(relationResult); + } + OnlineDatasourceRelation relation = relationResult.getData(); + List relationDataList = new LinkedList<>(); + relationDataMap.put(relation, relationDataList); + if (relation.getRelationType().equals(RelationType.ONE_TO_MANY)) { + JSONArray slaveObjectArray = slaveData.getJSONArray(key); + for (int i = 0; i < slaveObjectArray.size(); i++) { + relationDataList.add(slaveObjectArray.getJSONObject(i)); + } + } else if (relation.getRelationType().equals(RelationType.ONE_TO_ONE)) { + JSONObject o = slaveData.getJSONObject(key); + if (MapUtil.isNotEmpty(o)) { + relationDataList.add(o); + } + } + } + return ResponseResult.success(relationDataMap); + } + + /** + * 将字符型字段值转换为与参数字段类型匹配的字段值。 + * + * @param column 在线表单字段。 + * @param dataId 字符型字段值。 + * @return 转换后与参数字段类型匹配的字段值。 + */ + public Serializable convertToTypeValue(OnlineColumn column, String dataId) { + if (dataId == null) { + return null; + } + if (column == null) { + return dataId; + } + if ("Long".equals(column.getObjectFieldType())) { + return Long.valueOf(dataId); + } else if ("Integer".equals(column.getObjectFieldType())) { + return Integer.valueOf(dataId); + } + return dataId; + } + + /** + * 将字符型字段值集合转换为与参数字段类型匹配的字段值集合。 + * + * @param column 在线表单字段。 + * @param dataIdSet 字符型字段值集合。 + * @return 转换后与参数字段类型匹配的字段值集合。 + */ + public Set convertToTypeValue(OnlineColumn column, Set dataIdSet) { + Set resultSet = new HashSet<>(); + if (dataIdSet == null) { + return resultSet; + } + if ("Long".equals(column.getObjectFieldType())) { + return dataIdSet.stream().map(Long::valueOf).collect(Collectors.toSet()); + } else if ("Integer".equals(column.getObjectFieldType())) { + return dataIdSet.stream().map(Integer::valueOf).collect(Collectors.toSet()); + } else { + resultSet.addAll(dataIdSet); + } + return resultSet; + } + + /** + * 下载数据。 + * + * @param table 在线表对象。 + * @param dataId 在线表数据主键Id。 + * @param fieldName 数据表字段名。 + * @param filename 下载文件名。 + * @param asImage 是否为图片。 + * @param response HTTP 应对对象。 + */ + public void doDownload( + OnlineTable table, String dataId, String fieldName, String filename, Boolean asImage, HttpServletResponse response) { + // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。 + // 否则有可能给前端返回的是200的错误码。 + try { + // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。 + if (ObjectUtil.isEmpty(dataId)) { + if (!cacheHelper.existSessionUploadFile(filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } else { + Map dataMap = + onlineOperationService.getMasterData(table, null, null, dataId); + if (dataMap == null) { + ResponseResult.output(HttpServletResponse.SC_NOT_FOUND); + return; + } + String fieldJsonData = (String) dataMap.get(fieldName); + if (!this.canDownload(fieldJsonData, filename)) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + ResponseResult verifyResult = this.doVerifyUpDownloadFileColumn(table, fieldName, asImage); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, verifyResult); + return; + } + OnlineColumn downloadColumn = verifyResult.getData(); + if (downloadColumn.getUploadFileSystemType() == null) { + downloadColumn.setUploadFileSystemType(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal()); + } + if (!downloadColumn.getUploadFileSystemType().equals(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal())) { + downloadColumn.setUploadFileSystemType(onlineProperties.getDistributeStoreType()); + } + UploadStoreTypeEnum uploadStoreType = + UploadStoreTypeEnum.values()[downloadColumn.getUploadFileSystemType()]; + BaseUpDownloader upDownloader = upDownloaderFactory.get(uploadStoreType); + upDownloader.doDownload(onlineProperties.getUploadFileBaseDir(), + table.getModelName(), fieldName, filename, asImage, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error(e.getMessage(), e); + } + } + + /** + * 上传数据。 + * + * @param table 在线表对象。 + * @param fieldName 数据表字段名。 + * @param asImage 是否为图片。 + * @param uploadFile 上传的文件。 + */ + public void doUpload(OnlineTable table, String fieldName, Boolean asImage, MultipartFile uploadFile) + throws IOException { + ResponseResult verifyResult = this.doVerifyUpDownloadFileColumn(table, fieldName, asImage); + if (!verifyResult.isSuccess()) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, verifyResult); + return; + } + OnlineColumn uploadColumn = verifyResult.getData(); + if (uploadColumn.getUploadFileSystemType() == null) { + uploadColumn.setUploadFileSystemType(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal()); + } + if (!uploadColumn.getUploadFileSystemType().equals(UploadStoreTypeEnum.LOCAL_SYSTEM.ordinal())) { + uploadColumn.setUploadFileSystemType(onlineProperties.getDistributeStoreType()); + } + UploadStoreTypeEnum uploadStoreType = UploadStoreTypeEnum.values()[uploadColumn.getUploadFileSystemType()]; + BaseUpDownloader upDownloader = upDownloaderFactory.get(uploadStoreType); + UploadResponseInfo responseInfo = upDownloader.doUpload(null, + onlineProperties.getUploadFileBaseDir(), table.getModelName(), fieldName, asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, + ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage())); + return; + } + // 动态表单的下载url和普通表单有所不同,由前端负责动态拼接。 + responseInfo.setDownloadUri(null); + cacheHelper.putSessionUploadFile(responseInfo.getFilename()); + ResponseResult.output(ResponseResult.success(responseInfo)); + } + + private ResponseResult doVerifyUpDownloadFileColumn( + OnlineTable table, String fieldName, Boolean asImage) { + OnlineColumn column = this.getOnlineColumnByName(table, fieldName); + if (column == null) { + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD); + } + if (BooleanUtil.isTrue(asImage)) { + if (ObjectUtil.notEqual(column.getFieldKind(), FieldKind.UPLOAD_IMAGE)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD); + } + } else { + if (ObjectUtil.notEqual(column.getFieldKind(), FieldKind.UPLOAD)) { + return ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD); + } + } + return ResponseResult.success(column); + } + + private OnlineColumn getOnlineColumnByName(OnlineTable table, String fieldName) { + for (OnlineColumn column : table.getColumnMap().values()) { + if (column.getColumnName().equals(fieldName)) { + return column; + } + } + return null; + } + + private Object getColumnValue(JSONObject tableData, OnlineColumn column) { + Object value = tableData.get(column.getColumnName()); + if (value != null) { + if (ObjectFieldType.LONG.equals(column.getObjectFieldType())) { + value = Long.valueOf(value.toString()); + } else if (ObjectFieldType.DATE.equals(column.getObjectFieldType())) { + value = Convert.toLocalDateTime(value); + } + } + return value; + } + + private boolean isAutoSettingField(OnlineColumn column) { + return ObjectUtil.equal(column.getFieldKind(), FieldKind.CREATE_TIME) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.CREATE_USER_ID) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.UPDATE_TIME) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.UPDATE_USER_ID) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.CREATE_DEPT_ID) + || ObjectUtil.equal(column.getFieldKind(), FieldKind.LOGIC_DELETE); + } + + private boolean canDownload(String fieldJsonData, String filename) { + if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) { + return false; + } + return BaseUpDownloader.containFile(fieldJsonData, filename) + || cacheHelper.existSessionUploadFile(filename); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java new file mode 100644 index 00000000..431ae946 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineRedisKeyUtil.java @@ -0,0 +1,76 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单 Redis 键生成工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineRedisKeyUtil { + + /** + * 计算在线表对象缓存在Redis中的键值。 + * + * @param tableId 在线表主键Id。 + * @return 在线表对象缓存在Redis中的键值。 + */ + public static String makeOnlineTableKey(Long tableId) { + return "ONLINE_TABLE:" + tableId; + } + + /** + * 计算在线表单对象缓存在Redis中的键值。 + * + * @param formId 在线表单对象主键Id。 + * @return 在线表单对象缓存在Redis中的键值。 + */ + public static String makeOnlineFormKey(Long formId) { + return "ONLINE_FORM:" + formId; + } + + /** + * 计算在线表单关联数据源对象列表缓存在Redis中的键值。 + * + * @param formId 在线表单对象主键Id。 + * @return 在线表单关联数据源对象列表缓存在Redis中的键值。 + */ + public static String makeOnlineFormDatasourceKey(Long formId) { + return "ONLINE_FORM_DATASOURCE_LIST:" + formId; + } + + /** + * 计算在线数据源对象缓存在Redis中的键值。 + * + * @param datasourceId 在线数据源主键Id。 + * @return 在线数据源对象缓存在Redis中的键值。 + */ + public static String makeOnlineDataSourceKey(Long datasourceId) { + return "ONLINE_DATASOURCE:" + datasourceId; + } + + /** + * 计算在线数据源关联列表对象缓存在Redis中的键值。 + * + * @param datasourceId 在线数据源主键Id。 + * @return 在线数据源关联列表对象缓存在Redis中的键值。 + */ + public static String makeOnlineDataSourceRelationKey(Long datasourceId) { + return "ONLINE_DATASOURCE_RELATION:" + datasourceId; + } + + /** + * 计算在线字典对象缓存在Redis中的键值。 + * + * @param dictId 在线字典主键Id。 + * @return 在线字典对象缓存在Redis中的键值。 + */ + public static String makeOnlineDictKey(Long dictId) { + return "ONLINE_DICT:" + dictId; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private OnlineRedisKeyUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java new file mode 100644 index 00000000..712fe312 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/util/OnlineUtil.java @@ -0,0 +1,36 @@ +package com.orangeforms.common.online.util; + +/** + * 在线表单的工具类。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class OnlineUtil { + + /** + * 根据输入参数,拼接在线表单操作的查看权限字。 + * + * @param datasourceVariableName 数据源变量名。 + * @return 拼接后的在线表单操作的查看权限字。 + */ + public static String makeViewPermCode(String datasourceVariableName) { + return "online:" + datasourceVariableName + ":view"; + } + + /** + * 根据输入参数,拼接在线表单操作的编辑权限字。 + * + * @param datasourceVariableName 数据源变量名。 + * @return 拼接后的在线表单操作的编辑权限字。 + */ + public static String makeEditPermCode(String datasourceVariableName) { + return "online:" + datasourceVariableName + ":edit"; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private OnlineUtil() { + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java new file mode 100644 index 00000000..677eb67a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnRuleVo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 在线表单数据表字段规则和字段多对多关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段规则和字段多对多关联VO对象") +@Data +public class OnlineColumnRuleVo { + + /** + * 字段Id。 + */ + @Schema(description = "字段Id") + private Long columnId; + + /** + * 规则Id。 + */ + @Schema(description = "规则Id") + private Long ruleId; + + /** + * 规则属性数据。 + */ + @Schema(description = "规则属性数据") + private String propDataJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java new file mode 100644 index 00000000..3438eed4 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineColumnVo.java @@ -0,0 +1,204 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段规则和字段多对多关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段规则和字段多对多关联VO对象") +@Data +public class OnlineColumnVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long columnId; + + /** + * 字段名。 + */ + @Schema(description = "字段名") + private String columnName; + + /** + * 数据表Id。 + */ + @Schema(description = "数据表Id") + private Long tableId; + + /** + * 数据表中的字段类型。 + */ + @Schema(description = "数据表中的字段类型") + private String columnType; + + /** + * 数据表中的完整字段类型(包括了精度和刻度)。 + */ + @Schema(description = "数据表中的完整字段类型") + private String fullColumnType; + + /** + * 是否为主键。 + */ + @Schema(description = "是否为主键") + private Boolean primaryKey; + + /** + * 是否是自增主键(0: 不是 1: 是)。 + */ + @Schema(description = "是否是自增主键") + private Boolean autoIncrement; + + /** + * 是否可以为空 (0: 不可以为空 1: 可以为空)。 + */ + @Schema(description = "是否可以为空") + private Boolean nullable; + + /** + * 缺省值。 + */ + @Schema(description = "缺省值") + private String columnDefault; + + /** + * 字段在数据表中的显示位置。 + */ + @Schema(description = "字段在数据表中的显示位置") + private Integer columnShowOrder; + + /** + * 数据表中的字段注释。 + */ + @Schema(description = "数据表中的字段注释") + private String columnComment; + + /** + * 对象映射字段名称。 + */ + @Schema(description = "对象映射字段名称") + private String objectFieldName; + + /** + * 对象映射字段类型。 + */ + @Schema(description = "对象映射字段类型") + private String objectFieldType; + + /** + * 数值型字段的精度(目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的精度") + private Integer numericPrecision; + + /** + * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 + */ + @Schema(description = "数值型字段的刻度") + private Integer numericScale; + + /** + * 过滤类型。 + */ + @Schema(description = "过滤类型") + private Integer filterType; + + /** + * 是否是主键的父Id。 + */ + @Schema(description = "是否是主键的父Id") + private Boolean parentKey; + + /** + * 是否部门过滤字段。 + */ + @Schema(description = "是否部门过滤字段") + private Boolean deptFilter; + + /** + * 是否用户过滤字段。 + */ + @Schema(description = "是否用户过滤字段") + private Boolean userFilter; + + /** + * 字段类别。 + */ + @Schema(description = "字段类别") + private Integer fieldKind; + + /** + * 包含的文件文件数量,0表示无限制。 + */ + @Schema(description = "包含的文件文件数量,0表示无限制") + private Integer maxFileCount; + + /** + * 上传文件系统类型。 + */ + @Schema(description = "上传文件系统类型") + private Integer uploadFileSystemType; + + /** + * 编码规则的JSON格式数据。 + */ + @Schema(description = "编码规则的JSON格式数据") + private String encodedRule; + + /** + * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 + */ + @Schema(description = "脱敏字段类型") + private String maskFieldType; + + /** + * 字典Id。 + */ + @Schema(description = "字典Id") + private Long dictId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * fieldKind 常量字典关联数据。 + */ + @Schema(description = "常量字典关联数据") + private Map fieldKindDictMap; + + /** + * dictId 的一对一关联。 + */ + @Schema(description = "dictId 的一对一关联") + private Map dictInfo; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java new file mode 100644 index 00000000..6af755a9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceRelationVo.java @@ -0,0 +1,150 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单的数据源关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源关联VO对象") +@Data +public class OnlineDatasourceRelationVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long relationId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 关联名称。 + */ + @Schema(description = "关联名称") + private String relationName; + + /** + * 变量名。 + */ + @Schema(description = "变量名") + private String variableName; + + /** + * 主数据源Id。 + */ + @Schema(description = "主数据源Id") + private Long datasourceId; + + /** + * 关联类型。 + */ + @Schema(description = "关联类型") + private Integer relationType; + + /** + * 主表关联字段Id。 + */ + @Schema(description = "主表关联字段Id") + private Long masterColumnId; + + /** + * 从表Id。 + */ + @Schema(description = "从表Id") + private Long slaveTableId; + + /** + * 从表关联字段Id。 + */ + @Schema(description = "从表关联字段Id") + private Long slaveColumnId; + + /** + * 删除主表的时候是否级联删除一对一和一对多的从表数据,多对多只是删除关联,不受到这个标记的影响。。 + */ + @Schema(description = "一对多从表级联删除标记") + private Boolean cascadeDelete; + + /** + * 是否左连接。 + */ + @Schema(description = "是否左连接") + private Boolean leftJoin; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * masterColumnId 的一对一关联数据对象,数据对应类型为OnlineColumnVo。 + */ + @Schema(description = "masterColumnId字段的一对一关联数据对象") + private Map masterColumn; + + /** + * slaveTableId 的一对一关联数据对象,数据对应类型为OnlineTableVo。 + */ + @Schema(description = "slaveTableId字段的一对一关联数据对象") + private Map slaveTable; + + /** + * slaveColumnId 的一对一关联数据对象,数据对应类型为OnlineColumnVo。 + */ + @Schema(description = "slaveColumnId字段的一对一关联数据对象") + private Map slaveColumn; + + /** + * masterColumnId 字典关联数据。 + */ + @Schema(description = "masterColumnId的字典关联数据") + private Map masterColumnIdDictMap; + + /** + * slaveTableId 字典关联数据。 + */ + @Schema(description = "slaveTableId的字典关联数据") + private Map slaveTableIdDictMap; + + /** + * slaveColumnId 字典关联数据。 + */ + @Schema(description = "slaveColumnId的字典关联数据") + private Map slaveColumnIdDictMap; + + /** + * relationType 常量字典关联数据。 + */ + @Schema(description = "常量字典关联数据") + private Map relationTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java new file mode 100644 index 00000000..160432be --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDatasourceVo.java @@ -0,0 +1,97 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单的数据源VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据源VO对象") +@Data +public class OnlineDatasourceVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long datasourceId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 数据源名称。 + */ + @Schema(description = "数据源名称") + private String datasourceName; + + /** + * 数据源变量名,会成为数据访问url的一部分。 + */ + @Schema(description = "数据源变量名") + private String variableName; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 主表Id。 + */ + @Schema(description = "主表Id") + private Long masterTableId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * datasourceId 的多对多关联表数据对象,数据对应类型为OnlinePageDatasource。 + */ + @Schema(description = "datasourceId 的多对多关联表数据对象,数据对应类型为OnlinePageDatasource") + private Map onlinePageDatasource; + + /** + * masterTableId 字典关联数据。 + */ + @Schema(description = "masterTableId 字典关联数据") + private Map masterTableIdDictMap; + + /** + * 当前数据源及其关联,引用的数据表对象列表。 + */ + @Schema(description = "当前数据源及其关联,引用的数据表对象列表") + private List tableList; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java new file mode 100644 index 00000000..6415f31c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDblinkVo.java @@ -0,0 +1,84 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表所在数据库链接VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表所在数据库链接VO对象") +@Data +public class OnlineDblinkVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long dblinkId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 链接中文名称。 + */ + @Schema(description = "链接中文名称") + private String dblinkName; + + /** + * 链接描述。 + */ + @Schema(description = "链接描述") + private String dblinkDescription; + + /** + * 配置信息。 + */ + @Schema(description = "配置信息") + private String configuration; + + /** + * 数据库链接类型。 + */ + @Schema(description = "数据库链接类型") + private Integer dblinkType; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 数据库链接类型常量字典关联数据。 + */ + @Schema(description = "数据库链接类型常量字典关联数据") + private Map dblinkTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java new file mode 100644 index 00000000..804e5c71 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineDictVo.java @@ -0,0 +1,162 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单关联的字典VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单关联的字典VO对象") +@Data +public class OnlineDictVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long dictId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 字典名称。 + */ + @Schema(description = "字典名称") + private String dictName; + + /** + * 字典类型。 + */ + @Schema(description = "字典类型") + private Integer dictType; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 字典表名称。 + */ + @Schema(description = "字典表名称") + private String tableName; + + /** + * 全局字典编码。 + */ + @Schema(description = "全局字典编码") + private String dictCode; + + /** + * 逻辑删除字段。 + */ + @Schema(description = "逻辑删除字段") + private String deletedColumnName; + + /** + * 用户过滤滤字段名称。 + */ + @Schema(description = "用户过滤滤字段名称") + private String userFilterColumnName; + + /** + * 部门过滤字段名称。 + */ + @Schema(description = "部门过滤字段名称") + private String deptFilterColumnName; + + /** + * 租户过滤字段名称。 + */ + @Schema(description = "租户过滤字段名称") + private String tenantFilterColumnName; + + /** + * 字典表键字段名称。 + */ + @Schema(description = "字典表键字段名称") + private String keyColumnName; + + /** + * 字典表父键字段名称。 + */ + @Schema(description = "字典表父键字段名称") + private String parentKeyColumnName; + + /** + * 字典值字段名称。 + */ + @Schema(description = "字典值字段名称") + private String valueColumnName; + + /** + * 是否树形标记。 + */ + @Schema(description = "是否树形标记") + private Boolean treeFlag; + + /** + * 获取字典数据的url。 + */ + @Schema(description = "获取字典数据的url") + private String dictListUrl; + + /** + * 根据主键id批量获取字典数据的url。 + */ + @Schema(description = "根据主键id批量获取字典数据的url") + private String dictIdsUrl; + + /** + * 字典的JSON数据。 + */ + @Schema(description = "字典的JSON数据") + private String dictDataJson; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * dictType 常量字典关联数据。 + */ + @Schema(description = "dictType 常量字典关联数据") + private Map dictTypeDictMap; + + /** + * 数据库链接Id字典关联数据。 + */ + @Schema(description = "数据库链接Id字典关联数据") + private Map dblinkIdDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java new file mode 100644 index 00000000..d3373ce8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineFormVo.java @@ -0,0 +1,127 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 在线表单VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单VO对象") +@Data +public class OnlineFormVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long formId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 页面Id。 + */ + @Schema(description = "页面Id") + private Long pageId; + + /** + * 表单编码。 + */ + @Schema(description = "表单编码") + private String formCode; + + /** + * 表单名称。 + */ + @Schema(description = "表单名称") + private String formName; + + /** + * 表单类型。 + */ + @Schema(description = "表单类型") + private Integer formType; + + /** + * 表单类别。 + */ + @Schema(description = "表单类别") + private Integer formKind; + + /** + * 表单主表Id。 + */ + @Schema(description = "表单主表Id") + private Long masterTableId; + + /** + * 表单组件JSON。 + */ + @Schema(description = "表单组件JSON") + private String widgetJson; + + /** + * 表单参数JSON。 + */ + @Schema(description = "表单参数JSON") + private String paramsJson; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * masterTableId 的一对一关联数据对象,数据对应类型为OnlineTableVo。 + */ + @Schema(description = "asterTableId 的一对一关联数据对象") + private Map onlineTable; + + /** + * masterTableId 字典关联数据。 + */ + @Schema(description = "masterTableId 字典关联数据") + private Map masterTableIdDictMap; + + /** + * formType 常量字典关联数据。 + */ + @Schema(description = "formType 常量字典关联数据") + private Map formTypeDictMap; + + /** + * 当前表单关联的数据源Id集合。 + */ + @Schema(description = "当前表单关联的数据源Id集合") + private List datasourceIdList; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java new file mode 100644 index 00000000..adb113ff --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageDatasourceVo.java @@ -0,0 +1,33 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 在线表单页面和数据源多对多关联VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单页面和数据源多对多关联VO对象") +@Data +public class OnlinePageDatasourceVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long id; + + /** + * 页面主键Id。 + */ + @Schema(description = "页面主键Id") + private Long pageId; + + /** + * 数据源主键Id。 + */ + @Schema(description = "数据源主键Id") + private Long datasourceId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java new file mode 100644 index 00000000..bd80de12 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlinePageVo.java @@ -0,0 +1,96 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单所在页面VO对象。这里我们可以把页面理解为表单的容器。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单所在页面VO对象") +@Data +public class OnlinePageVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long pageId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 页面编码。 + */ + @Schema(description = "页面编码") + private String pageCode; + + /** + * 页面名称。 + */ + @Schema(description = "页面名称") + private String pageName; + + /** + * 页面类型。 + */ + @Schema(description = "页面类型") + private Integer pageType; + + /** + * 页面编辑状态。 + */ + @Schema(description = "页面编辑状态") + private Integer status; + + /** + * 是否发布。 + */ + @Schema(description = "是否发布") + private Boolean published; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * pageType 常量字典关联数据。 + */ + @Schema(description = "pageType 常量字典关联数据") + private Map pageTypeDictMap; + + /** + * status 常量字典关联数据。 + */ + @Schema(description = "status 常量字典关联数据") + private Map statusDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java new file mode 100644 index 00000000..ba88dbec --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineRuleVo.java @@ -0,0 +1,90 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 在线表单数据表字段验证规则VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单数据表字段验证规则VO对象") +@Data +public class OnlineRuleVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long ruleId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用编码。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 规则名称。 + */ + @Schema(description = "规则名称") + private String ruleName; + + /** + * 规则类型。 + */ + @Schema(description = "规则类型") + private Integer ruleType; + + /** + * 内置规则标记。 + */ + @Schema(description = "内置规则标记") + private Boolean builtin; + + /** + * 自定义规则的正则表达式。 + */ + @Schema(description = "自定义规则的正则表达式") + private String pattern; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; + + /** + * ruleId 的多对多关联表数据对象,数据对应类型为OnlineColumnRuleVo。 + */ + @Schema(description = "ruleId 的多对多关联表数据对象") + private Map onlineColumnRule; + + /** + * ruleType 常量字典关联数据。 + */ + @Schema(description = "ruleType 常量字典关联数据") + private Map ruleTypeDictMap; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java new file mode 100644 index 00000000..66561baf --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineTableVo.java @@ -0,0 +1,71 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 在线表单的数据表VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线表单的数据表VO对象") +@Data +public class OnlineTableVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long tableId; + + /** + * 应用编码。为空时,表示非第三方应用接入。 + */ + @Schema(description = "应用。为空时,表示非第三方应用接入") + private String appCode; + + /** + * 表名称。 + */ + @Schema(description = "表名称") + private String tableName; + + /** + * 实体名称。 + */ + @Schema(description = "实体名称") + private String modelName; + + /** + * 数据库链接Id。 + */ + @Schema(description = "数据库链接Id") + private Long dblinkId; + + /** + * 创建时间。 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建者。 + */ + @Schema(description = "创建者") + private Long createUserId; + + /** + * 更新时间。 + */ + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 更新者。 + */ + @Schema(description = "更新者") + private Long updateUserId; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java new file mode 100644 index 00000000..2a4ca215 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/java/com/orangeforms/common/online/vo/OnlineVirtualColumnVo.java @@ -0,0 +1,87 @@ +package com.orangeforms.common.online.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 在线数据表虚拟字段VO对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Schema(description = "在线数据表虚拟字段VO对象") +@Data +public class OnlineVirtualColumnVo { + + /** + * 主键Id。 + */ + @Schema(description = "主键Id") + private Long virtualColumnId; + + /** + * 所在表Id。 + */ + @Schema(description = "所在表Id") + private Long tableId; + + /** + * 字段名称。 + */ + @Schema(description = "字段名称") + private String objectFieldName; + + /** + * 属性类型。 + */ + @Schema(description = "属性类型") + private String objectFieldType; + + /** + * 字段提示名。 + */ + @Schema(description = "字段提示名") + private String columnPrompt; + + /** + * 虚拟字段类型(0: 聚合)。 + */ + @Schema(description = "虚拟字段类型(0: 聚合)") + private Integer virtualType; + + /** + * 关联数据源Id。 + */ + @Schema(description = "关联数据源Id") + private Long datasourceId; + + /** + * 关联Id。 + */ + @Schema(description = "关联Id") + private Long relationId; + + /** + * 聚合字段所在关联表Id。 + */ + @Schema(description = "聚合字段所在关联表Id") + private Long aggregationTableId; + + /** + * 关联表聚合字段Id。 + */ + @Schema(description = "关联表聚合字段Id") + private Long aggregationColumnId; + + /** + * 聚合类型(0: count 1: sum 2: avg 3: max 4:min)。 + */ + @Schema(description = "聚合类型(0: count 1: sum 2: avg 3: max 4:min)") + private Integer aggregationType; + + /** + * 存储过滤条件的json。 + */ + @Schema(description = "存储过滤条件的json") + private String whereClauseJson; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..d9cb5fb0 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-online/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.online.config.OnlineAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-redis/pom.xml new file mode 100644 index 00000000..c0fe169d --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/pom.xml @@ -0,0 +1,29 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-redis + 1.0.0 + common-redis + jar + + + + com.orangeforms + common-core + 1.0.0 + + + org.redisson + redisson + ${redisson.version} + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java new file mode 100644 index 00000000..da1c2fc2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisDictionaryCache.java @@ -0,0 +1,263 @@ +package com.orangeforms.common.redis.cache; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.cache.DictionaryCache; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.RedisCacheAccessException; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 字典数据Redis缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class RedisDictionaryCache implements DictionaryCache { + + /** + * 字典数据前缀,便于Redis工具分组显示。 + */ + protected static final String DICT_PREFIX = "DICT-TABLE:"; + /** + * redisson客户端。 + */ + protected final RedissonClient redissonClient; + /** + * 数据存储对象。 + */ + protected final RMap dataMap; + /** + * 字典值对象类型。 + */ + protected final Class valueClazz; + /** + * 获取字典主键数据的函数对象。 + */ + protected final Function idGetter; + + /** + * 当前对象的构造器函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的字典内存缓存对象。 + */ + public static RedisDictionaryCache create( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + return new RedisDictionaryCache<>(redissonClient, dictionaryName, valueClazz, idGetter); + } + + /** + * 构造函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。确保全局唯一。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + */ + public RedisDictionaryCache( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter) { + this.redissonClient = redissonClient; + this.dataMap = redissonClient.getMap( + DICT_PREFIX + dictionaryName + ApplicationConstant.DICT_CACHE_NAME_SUFFIX); + this.valueClazz = valueClazz; + this.idGetter = idGetter; + } + + protected RMap getDataMap() { + return dataMap; + } + + @Override + public List getAll() { + Collection dataList; + String exceptionMessage; + try { + dataList = getDataMap().readAllValues(); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::getAll] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (dataList == null) { + return new LinkedList<>(); + } + return dataList.stream() + .map(data -> JSON.parseObject(data, valueClazz)) + .collect(Collectors.toCollection(LinkedList::new)); + } + + @Override + public List getInList(Set keys) { + if (CollUtil.isEmpty(keys)) { + return new LinkedList<>(); + } + Collection dataList; + String exceptionMessage; + try { + dataList = getDataMap().getAll(keys).values(); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::getInList] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (dataList == null) { + return new LinkedList<>(); + } + return dataList.stream() + .map(data -> JSON.parseObject(data, valueClazz)) + .collect(Collectors.toCollection(LinkedList::new)); + } + + @Override + public V get(K id) { + if (id == null) { + return null; + } + String data; + String exceptionMessage; + try { + data = getDataMap().get(id); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::get] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (data == null) { + return null; + } + return JSON.parseObject(data, valueClazz); + } + + @Override + public int getCount() { + return getDataMap().size(); + } + + @Override + public void put(K id, V data) { + if (id == null || data == null) { + return; + } + String exceptionMessage; + try { + getDataMap().fastPut(id, JSON.toJSONString(data)); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::put] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void reload(List dataList, boolean force) { + String exceptionMessage; + try { + // 如果不强制刷新,需要先判断缓存中是否存在数据。 + if (!force && this.getCount() > 0) { + return; + } + Map map = null; + if (CollUtil.isNotEmpty(dataList)) { + map = dataList.stream().collect(Collectors.toMap(idGetter, JSON::toJSONString)); + } + RMap localDataMap = getDataMap(); + localDataMap.clear(); + if (map != null) { + localDataMap.putAll(map); + } + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::reload] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + String data; + String exceptionMessage; + try { + data = getDataMap().remove(id); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::invalidate] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (data == null) { + return null; + } + return JSON.parseObject(data, valueClazz); + } + + @SuppressWarnings("unchecked") + @Override + public void invalidateSet(Set keys) { + if (CollUtil.isEmpty(keys)) { + return; + } + Object[] keyArray = keys.toArray(new Object[]{}); + String exceptionMessage; + try { + getDataMap().fastRemove((K[]) keyArray); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::invalidateSet] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void invalidateAll() { + String exceptionMessage; + try { + getDataMap().clear(); + } catch (Exception e) { + exceptionMessage = String.format( + "[%s::invalidateAll] encountered EXCEPTION [%s] for DICT [%s].", + this.getClass().getSimpleName(), e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java new file mode 100644 index 00000000..de910c61 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedisTreeDictionaryCache.java @@ -0,0 +1,224 @@ +package com.orangeforms.common.redis.cache; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.exception.RedisCacheAccessException; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import org.redisson.api.RListMultimap; +import org.redisson.api.RedissonClient; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 树形字典数据Redis缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +public class RedisTreeDictionaryCache extends RedisDictionaryCache { + + /** + * 树形数据存储对象。 + */ + private final RListMultimap allTreeMap; + /** + * 获取字典父主键数据的函数对象。 + */ + protected final Function parentIdGetter; + + /** + * 当前对象的构造器函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的树形字典内存缓存对象。 + */ + public static RedisTreeDictionaryCache create( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter, + Function parentIdGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + if (parentIdGetter == null) { + throw new IllegalArgumentException("ParentIdGetter can't be NULL."); + } + return new RedisTreeDictionaryCache<>( + redissonClient, dictionaryName, valueClazz, idGetter, parentIdGetter); + } + + /** + * 构造函数。 + * + * @param redissonClient Redisson的客户端对象。 + * @param dictionaryName 字典表的名称。等同于redis hash对象的key。 + * @param valueClazz 值对象的Class对象。 + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + */ + public RedisTreeDictionaryCache( + RedissonClient redissonClient, + String dictionaryName, + Class valueClazz, + Function idGetter, + Function parentIdGetter) { + super(redissonClient, dictionaryName, valueClazz, idGetter); + this.allTreeMap = redissonClient.getListMultimap( + DICT_PREFIX + dictionaryName + ApplicationConstant.TREE_DICT_CACHE_NAME_SUFFIX); + this.parentIdGetter = parentIdGetter; + } + + @Override + public List getListByParentId(K parentId) { + List dataList; + String exceptionMessage; + try { + dataList = allTreeMap.get(parentId); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::getListByParentId] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(data -> JSON.parseObject(data, valueClazz)).collect(Collectors.toList()); + } + + @Override + public void reload(List dataList, boolean force) { + String exceptionMessage; + try { + // 如果不强制刷新,需要先判断缓存中是否存在数据。 + if (!force && this.getCount() > 0) { + return; + } + dataMap.clear(); + allTreeMap.clear(); + if (CollUtil.isEmpty(dataList)) { + return; + } + Map map = dataList.stream().collect(Collectors.toMap(idGetter, JSON::toJSONString)); + // 这里现在本地内存构建树形数据关系,然后再批量存入到Redis缓存。 + // 以便减少与Redis的交互,同时提升运行时效率。 + Multimap treeMap = LinkedListMultimap.create(); + for (V data : dataList) { + treeMap.put(parentIdGetter.apply(data), JSON.toJSONString(data)); + } + dataMap.putAll(map, 3000); + for (Map.Entry> entry : treeMap.asMap().entrySet()) { + allTreeMap.putAll(entry.getKey(), entry.getValue()); + } + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisDictionaryCache::reload] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void put(K id, V data) { + if (id == null || data == null) { + return; + } + String stringData = JSON.toJSONString(data); + K parentId = parentIdGetter.apply(data); + String exceptionMessage; + try { + String oldData = dataMap.put(id, stringData); + if (oldData != null) { + allTreeMap.remove(parentId, oldData); + } + allTreeMap.put(parentId, stringData); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::put] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + V data = null; + String exceptionMessage; + try { + String stringData = dataMap.remove(id); + if (stringData != null) { + data = JSON.parseObject(stringData, valueClazz); + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, stringData); + } + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::invalidate] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + return data; + } + + @Override + public void invalidateSet(Set keys) { + if (CollUtil.isEmpty(keys)) { + return; + } + String exceptionMessage; + try { + keys.forEach(id -> { + if (id != null) { + String stringData = dataMap.remove(id); + if (stringData != null) { + K parentId = parentIdGetter.apply(JSON.parseObject(stringData, valueClazz)); + allTreeMap.remove(parentId, stringData); + } + } + }); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::invalidateSet] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } + + @Override + public void invalidateAll() { + String exceptionMessage; + try { + dataMap.clear(); + allTreeMap.clear(); + } catch (Exception e) { + exceptionMessage = String.format( + "Operation of [RedisTreeDictionaryCache::invalidateAll] encountered EXCEPTION [%s] for DICT [%s].", + e.getClass().getSimpleName(), valueClazz.getSimpleName()); + log.warn(exceptionMessage); + throw new RedisCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java new file mode 100644 index 00000000..5210be88 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/RedissonCacheConfig.java @@ -0,0 +1,73 @@ +package com.orangeforms.common.redis.cache; + +import com.google.common.collect.Maps; +import org.redisson.api.RedissonClient; +import org.redisson.spring.cache.CacheConfig; +import org.redisson.spring.cache.RedissonSpringCacheManager; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.util.Map; + +/** + * 使用Redisson作为Redis的分布式缓存库。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@EnableCaching +public class RedissonCacheConfig { + + private static final int DEFAULT_TTL = 3600000; + + /** + * 定义cache名称、超时时长(毫秒)。 + */ + public enum CacheEnum { + /** + * session下上传文件名的缓存(时间是24小时)。 + */ + UPLOAD_FILENAME_CACHE(86400000), + /** + * session的打印访问令牌缓存(时间是1小时)。 + */ + PRINT_ACCESS_TOKEN_CACHE(3600000), + /** + * 缺省全局缓存(时间是24小时)。 + */ + GLOBAL_CACHE(86400000); + + /** + * 缓存的时长(单位:毫秒) + */ + private int ttl = DEFAULT_TTL; + + CacheEnum() { + } + + CacheEnum(int ttl) { + this.ttl = ttl; + } + + public int getTtl() { + return ttl; + } + } + + /** + * 初始化缓存配置。 + */ + @Bean + @Primary + public CacheManager cacheManager(RedissonClient redissonClient) { + Map config = Maps.newHashMap(); + for (CacheEnum c : CacheEnum.values()) { + config.put(c.name(), new CacheConfig(c.getTtl(), 0)); + } + return new RedissonSpringCacheManager(redissonClient, config); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java new file mode 100644 index 00000000..4c613c7c --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/cache/SessionCacheHelper.java @@ -0,0 +1,179 @@ +package com.orangeforms.common.redis.cache; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import com.alibaba.fastjson.JSON; +import com.orangeforms.common.core.object.MyPrintInfo; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.exception.MyRuntimeException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Session数据缓存辅助类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@SuppressWarnings("unchecked") +@Component +public class SessionCacheHelper { + + @Autowired + private CacheManager cacheManager; + + private static final String NO_CACHE_FORMAT_MSG = "No redisson cache [{}]!"; + + /** + * 缓存当前session内,上传过的文件名。 + * + * @param filename 通常是本地存储的文件名,而不是上传时的原始文件名。 + */ + public void putSessionUploadFile(String filename) { + if (filename != null) { + Set sessionUploadFileSet = null; + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + throw new MyRuntimeException(msg); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper != null) { + sessionUploadFileSet = (Set) valueWrapper.get(); + } + if (sessionUploadFileSet == null) { + sessionUploadFileSet = new HashSet<>(); + } + sessionUploadFileSet.add(filename); + cache.put(TokenData.takeFromRequest().getSessionId(), sessionUploadFileSet); + } + } + + /** + * 缓存当前Session可以下载的文件集合。 + * + * @param filenameSet 后台服务本地存储的文件名,而不是上传时的原始文件名。 + */ + public void putSessionDownloadableFileNameSet(Set filenameSet) { + if (CollUtil.isEmpty(filenameSet)) { + return; + } + Set sessionUploadFileSet = null; + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + if (cache == null) { + throw new MyRuntimeException(StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name())); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper != null) { + sessionUploadFileSet = (Set) valueWrapper.get(); + } + if (sessionUploadFileSet == null) { + sessionUploadFileSet = new HashSet<>(); + } + sessionUploadFileSet.addAll(filenameSet); + cache.put(TokenData.takeFromRequest().getSessionId(), sessionUploadFileSet); + } + + /** + * 判断参数中的文件名,是否有当前session上传。 + * + * @param filename 通常是本地存储的文件名,而不是上传时的原始文件名。 + * @return true表示该文件是由当前session上传并存储在本地的,否则false。 + */ + public boolean existSessionUploadFile(String filename) { + if (filename == null) { + return false; + } + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name()); + throw new MyRuntimeException(msg); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper == null) { + return false; + } + Object cachedData = valueWrapper.get(); + if (cachedData == null) { + return false; + } + return ((Set) cachedData).contains(filename); + } + + /** + * 缓存当前session内,可打印的安全令牌。 + * + * @param token 打印安全令牌。 + * @param printInfo 打印参数信息。 + */ + public void putSessionPrintTokenAndInfo(String token, MyPrintInfo printInfo) { + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + throw new MyRuntimeException(msg); + } + Map sessionPrintTokenMap = null; + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper != null) { + sessionPrintTokenMap = (Map) valueWrapper.get(); + } + if (sessionPrintTokenMap == null) { + sessionPrintTokenMap = new HashMap<>(4); + } + sessionPrintTokenMap.put(token, JSON.toJSONString(printInfo)); + cache.put(TokenData.takeFromRequest().getSessionId(), sessionPrintTokenMap); + } + + /** + * 获取当前session中,指定打印令牌所关联的打印信息。 + * + * @param token 打印安全令牌。 + * @return 当前session中,指定打印令牌所关联的打印信息。不存在返回null。 + */ + public MyPrintInfo getSessionPrintInfoByToken(String token) { + Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + if (cache == null) { + String msg = StrFormatter.format(NO_CACHE_FORMAT_MSG, + RedissonCacheConfig.CacheEnum.PRINT_ACCESS_TOKEN_CACHE.name()); + throw new MyRuntimeException(msg); + } + Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId()); + if (valueWrapper == null) { + return null; + } + Object cachedData = valueWrapper.get(); + if (cachedData == null) { + return null; + } + String data = ((Map) cachedData).get(token); + if (data == null) { + return null; + } + return JSON.parseObject(data, MyPrintInfo.class); + } + + /** + * 清除当前session的所有缓存数据。 + * + * @param sessionId 当前会话的SessionId。 + */ + public void removeAllSessionCache(String sessionId) { + for (RedissonCacheConfig.CacheEnum c : RedissonCacheConfig.CacheEnum.values()) { + Cache cache = cacheManager.getCache(c.name()); + if (cache != null) { + cache.evict(sessionId); + } + } + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java new file mode 100644 index 00000000..fecec4b9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/config/RedissonConfig.java @@ -0,0 +1,105 @@ +package com.orangeforms.common.redis.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.exception.InvalidRedisModeException; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Redisson配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Configuration +@ConditionalOnProperty(name = "common-redis.redisson.enabled", havingValue = "true") +public class RedissonConfig { + + @Value("${common-redis.redisson.lockWatchdogTimeout}") + private Integer lockWatchdogTimeout; + + @Value("${common-redis.redisson.mode}") + private String mode; + + /** + * 仅仅用于sentinel模式。 + */ + @Value("${common-redis.redisson.masterName:}") + private String masterName; + + @Value("${common-redis.redisson.address}") + private String address; + + @Value("${common-redis.redisson.timeout}") + private Integer timeout; + + @Value("${common-redis.redisson.password:}") + private String password; + + @Value("${common-redis.redisson.pool.poolSize}") + private Integer poolSize; + + @Value("${common-redis.redisson.pool.minIdle}") + private Integer minIdle; + + @Bean + public RedissonClient redissonClient() { + if (StrUtil.isBlank(password)) { + password = null; + } + Config config = new Config(); + if ("single".equals(mode)) { + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useSingleServer() + .setPassword(password) + .setAddress(address) + .setConnectionPoolSize(poolSize) + .setConnectionMinimumIdleSize(minIdle) + .setConnectTimeout(timeout); + } else if ("cluster".equals(mode)) { + String[] clusterAddresses = StrUtil.splitToArray(address, ','); + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useClusterServers() + .setPassword(password) + .addNodeAddress(clusterAddresses) + .setConnectTimeout(timeout) + .setMasterConnectionPoolSize(poolSize) + .setMasterConnectionMinimumIdleSize(minIdle); + } else if ("sentinel".equals(mode)) { + String[] sentinelAddresses = StrUtil.splitToArray(address, ','); + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useSentinelServers() + .setPassword(password) + .setMasterName(masterName) + .addSentinelAddress(sentinelAddresses) + .setConnectTimeout(timeout) + .setMasterConnectionPoolSize(poolSize) + .setMasterConnectionMinimumIdleSize(minIdle); + } else if ("master-slave".equals(mode)) { + String[] masterSlaveAddresses = StrUtil.splitToArray(address, ','); + if (masterSlaveAddresses.length == 1) { + throw new IllegalArgumentException( + "redis.redisson.address MUST have multiple redis addresses for master-slave mode."); + } + String[] slaveAddresses = new String[masterSlaveAddresses.length - 1]; + ArrayUtil.copy(masterSlaveAddresses, 1, slaveAddresses, 0, slaveAddresses.length); + config.setLockWatchdogTimeout(lockWatchdogTimeout) + .useMasterSlaveServers() + .setPassword(password) + .setMasterAddress(masterSlaveAddresses[0]) + .addSlaveAddress(slaveAddresses) + .setConnectTimeout(timeout) + .setMasterConnectionPoolSize(poolSize) + .setMasterConnectionMinimumIdleSize(minIdle); + } else { + throw new InvalidRedisModeException(mode); + } + return Redisson.create(config); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java new file mode 100644 index 00000000..0ffd4414 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/java/com/orangeforms/common/redis/util/CommonRedisUtil.java @@ -0,0 +1,217 @@ +package com.orangeforms.common.redis.util; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RAtomicLong; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * Redis的常用工具方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Slf4j +@Component +public class CommonRedisUtil { + + @Autowired + private RedissonClient redissonClient; + + private static final Integer DEFAULT_EXPIRE_SECOND = 300; + + /** + * 计算流水号前缀部分。 + * + * @param prefix 前缀字符串。 + * @param precisionTo 精确到的时间单元,目前仅仅支持 YEAR/MONTH/DAYS/HOURS/MINUTES/SECONDS。 + * @param middle 日期和流水号之间的字符串。 + * @return 返回计算后的前缀部分。 + */ + public String calculateTransIdPrefix(String prefix, String precisionTo, String middle) { + String key = prefix; + if (key == null) { + key = ""; + } + DateTime dateTime = new DateTime(); + String fmt = "yyyy"; + String fmt2 = fmt + "MMddHH"; + switch (precisionTo) { + case "YEAR": + break; + case "MONTH": + fmt += "MM"; + break; + case "DAYS": + fmt = fmt + "MMdd"; + break; + case "HOURS": + fmt = fmt2; + break; + case "MINUTES": + fmt = fmt2 + "mm"; + break; + case "SECONDS": + fmt = fmt2 + "mmss"; + break; + default: + throw new UnsupportedOperationException("Only Support YEAR/MONTH/DAYS/HOURS/MINUTES/SECONDS"); + } + key += dateTime.toString(fmt); + return middle != null ? key + middle : key; + } + + /** + * 生成基于时间的流水号方法。 + * + * @param prefix 前缀字符串。 + * @param precisionTo 精确到的时间单元,目前仅仅支持 YEAR/MONTH/DAYS/HOURS/MINUTES/SECONDS。 + * @param middle 日期和流水号之间的字符串。 + * @param idWidth 计算出的流水号宽度,前面补充0。比如idWidth = 3, 输出值为 005/012/123。 + * 需要注意的是,流水号值超出idWidth指定宽度,低位会被截取。 + * @return 基于时间的流水号方法。 + */ + public String generateTransId(String prefix, String precisionTo, String middle, int idWidth) { + TimeUnit unit = EnumUtil.fromString(TimeUnit.class, precisionTo, null); + int unitCount = 1; + if (unit == null) { + unit = TimeUnit.DAYS; + DateTime now = DateTime.now(); + if (StrUtil.equals(precisionTo, "MONTH")) { + DateTime endOfMonthDay = DateUtil.endOfMonth(now); + unitCount = endOfMonthDay.getField(DateField.DAY_OF_MONTH) - now.getField(DateField.DAY_OF_MONTH) + 1; + } else if (StrUtil.equals(precisionTo, "YEAR")) { + DateTime endOfYearDay = DateUtil.endOfYear(now); + unitCount = endOfYearDay.getField(DateField.DAY_OF_YEAR) - now.getField(DateField.DAY_OF_YEAR) + 1; + } + } + String key = this.calculateTransIdPrefix(prefix, precisionTo, middle); + RAtomicLong atomicLong = redissonClient.getAtomicLong(key); + long value = atomicLong.incrementAndGet(); + if (value == 1L) { + atomicLong.expire(unitCount, unit); + } + return key + StrUtil.padPre(String.valueOf(value), idWidth, "0"); + } + + /** + * 为指定的键设置流水号的初始值。 + * + * @param key 指定的键。 + * @param initalValue 初始值。 + */ + public void initTransId(String key, Long initalValue) { + RAtomicLong atomicLong = redissonClient.getAtomicLong(key); + atomicLong.set(initalValue); + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param id 数据Id。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @return 数据对象。 + */ + public M getFromCache(String key, Serializable id, Function f, Class clazz) { + return this.getFromCache(key, id, f, clazz, null); + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param filter mybatis plus的过滤对象。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @return 数据对象。 + */ + public N getFromCacheWithQueryWrapper( + String key, LambdaQueryWrapper filter, Function, N> f, Class clazz) { + N m; + RBucket bucket = redissonClient.getBucket(key); + if (!bucket.isExists()) { + m = f.apply(filter); + if (m != null) { + bucket.set(JSON.toJSONString(m), DEFAULT_EXPIRE_SECOND, TimeUnit.SECONDS); + } + } else { + m = JSON.parseObject(bucket.get(), clazz); + } + return m; + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param filter 过滤对象。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @return 数据对象。 + */ + public N getFromCache(String key, M filter, Function f, Class clazz) { + N m; + RBucket bucket = redissonClient.getBucket(key); + if (!bucket.isExists()) { + m = f.apply(filter); + if (m != null) { + bucket.set(JSON.toJSONString(m), DEFAULT_EXPIRE_SECOND, TimeUnit.SECONDS); + } + } else { + m = JSON.parseObject(bucket.get(), clazz); + } + return m; + } + + /** + * 从缓存中获取数据。如果缓存中不存在则从执行指定的方法获取数据,并将得到的数据同步到缓存。 + * + * @param key 缓存的键。 + * @param id 数据Id。 + * @param f 获取数据的方法。 + * @param clazz 数据对象类型。 + * @param seconds 过期秒数。 + * @return 数据对象。 + */ + public M getFromCache( + String key, Serializable id, Function f, Class clazz, Integer seconds) { + M m; + RBucket bucket = redissonClient.getBucket(key); + if (!bucket.isExists()) { + m = f.apply(id); + if (m != null) { + if (seconds == null) { + seconds = DEFAULT_EXPIRE_SECOND; + } + bucket.set(JSON.toJSONString(m), seconds, TimeUnit.SECONDS); + } + } else { + m = JSON.parseObject(bucket.get(), clazz); + } + return m; + } + + /** + * 移除指定Key。 + * + * @param key 键名。 + */ + public void evictFormCache(String key) { + redissonClient.getBucket(key).delete(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..1cac49fc --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.redis.config.RedissonConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-satoken/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-satoken/pom.xml new file mode 100644 index 00000000..d2b782dd --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-satoken/pom.xml @@ -0,0 +1,49 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-satoken + 1.0.0 + common-satoken + jar + + 1.37.0 + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + cn.dev33 + sa-token-redis-fastjson + ${sa-token.version} + + + + cn.dev33 + sa-token-alone-redis + ${sa-token.version} + + + + org.apache.commons + commons-pool2 + + + com.orangeforms + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java new file mode 100644 index 00000000..8838858f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/annotation/SaTokenDenyAuth.java @@ -0,0 +1,16 @@ +package com.orangeforms.common.satoken.annotation; + +import java.lang.annotation.*; + +/** + * 所有标记该注解的接口,不能使用SaToken进行权限验证。 + * 必须通过橙单自身的动态验证完成,即基于URL的验证。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SaTokenDenyAuth { +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java new file mode 100644 index 00000000..662bd7e7 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/listener/SaTokenPermCodeScanListener.java @@ -0,0 +1,26 @@ +package com.orangeforms.common.satoken.listener; + +import com.orangeforms.common.satoken.util.SaTokenUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +/** + * 后台服务启动的时候扫描服务中标有权限字,并同步到Redis,以供接口查询所有使用到的权限字。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class SaTokenPermCodeScanListener implements ApplicationListener { + + @Autowired + private SaTokenUtil saTokenUtil; + + @Override + public void onApplicationEvent(@NonNull ApplicationReadyEvent event) { + saTokenUtil.collectPermCodes(event); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java new file mode 100644 index 00000000..750c3a4a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/SaTokenUtil.java @@ -0,0 +1,283 @@ +package com.orangeforms.common.satoken.util; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.constant.ApplicationConstant; +import com.orangeforms.common.core.constant.ErrorCodeEnum; +import com.orangeforms.common.core.object.LoginUserInfo; +import com.orangeforms.common.core.object.ResponseResult; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.AopTargetUtil; +import com.orangeforms.common.core.util.MyCommonUtil; +import com.orangeforms.common.core.util.RedisKeyUtil; +import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth; +import org.redisson.api.RMap; +import org.redisson.api.RSet; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 通用工具方法。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class SaTokenUtil { + + @Autowired + private RedissonClient redissonClient; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + @Value("${spring.application.name}") + private String applicationName; + + public static final String SA_TOKEN_PERM_CODES_KEY = "SaTokenPermCodes"; + public static final String SA_TOKEN_PERM_CODES_PUBLISH_TOPIC = "SaTokenPermCodesTopic"; + + /** + * 处理免验证接口。目前仅用于微服务的业务服务。 + */ + public void handleNoAuthIntercept() { + if (!StpUtil.isLogin()) { + return; + } + SaSession session = StpUtil.getTokenSession(); + if (session != null) { + TokenData tokenData = JSON.toJavaObject( + (JSONObject) session.get(TokenData.REQUEST_ATTRIBUTE_NAME), TokenData.class); + TokenData.addToRequest(tokenData); + tokenData.setToken(session.getToken()); + } + } + + /** + * 处理权限验证,通常在拦截器中调用。用于微服务中业务服务。 + * + * @param request 当前请求。 + * @param handler 拦截器中的处理器。 + * @return 拦截验证处理结果。 + */ + public ResponseResult handleAuthInterceptEx(HttpServletRequest request, Object handler) { + String appCode = MyCommonUtil.getAppCodeFromRequest(); + if (StrUtil.isNotBlank(appCode)) { + String token = request.getHeader(TokenData.REQUEST_ATTRIBUTE_NAME); + if (StrUtil.isBlank(token)) { + String errorMessage = "第三方登录没有包含Token信息!"; + return ResponseResult.error( + HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage); + } + TokenData tokenData = JSON.parseObject(token, TokenData.class); + TokenData.addToRequest(tokenData); + return ResponseResult.success(); + } + String dontAuth = request.getHeader(ApplicationConstant.HTTP_HEADER_DONT_AUTH); + if (BooleanUtil.toBoolean(dontAuth)) { + this.handleNoAuthIntercept(); + return ResponseResult.success(); + } + return this.handleAuthIntercept(request, handler); + } + + /** + * 处理权限验证,通常在拦截器中调用。通常用于单体服务。 + * + * @param request 当前请求。 + * @param handler 拦截器中的处理器。 + * @return 拦截验证处理结果。 + */ + public ResponseResult handleAuthIntercept(HttpServletRequest request, Object handler) { + if (!(handler instanceof HandlerMethod)) { + return ResponseResult.success(); + } + Method method = ((HandlerMethod) handler).getMethod(); + String errorMessage; + //如果没有登录则直接交给satoken注解去验证。 + if (!StpUtil.isLogin()) { + // 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权 + if (BooleanUtil.isTrue(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class))) { + return ResponseResult.success(); + } + errorMessage = "非免登录接口必须包含Token信息!"; + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage); + } + //对于已经登录的用户一定存在session对象。 + SaSession session = StpUtil.getTokenSession(); + if (session == null) { + errorMessage = "用户会话已过期,请重新登录!"; + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage); + } + TokenData tokenData = JSON.toJavaObject( + (JSONObject) session.get(TokenData.REQUEST_ATTRIBUTE_NAME), TokenData.class); + TokenData.addToRequest(tokenData); + //将最初前端请求使用的token数据赋值给TokenData对象,以便于再次调用其他API接口时直接使用。 + tokenData.setToken(session.getToken()); + //如果是管理员可以直接跳过验证了。 + //基于橙单内部的权限规则优先验证,主要用于内部的白名单接口,以及在线表单和工作流那些动态接口的权限验证。 + if (Boolean.TRUE.equals(tokenData.getIsAdmin()) + || this.hasPermission(tokenData.getSessionId(), request.getRequestURI())) { + return ResponseResult.success(); + } + //对于应由白名单鉴权的接口,都会添加SaTokenDenyAuth注解,因此这里需要判断一下, + //对于此类接口无需SaToken验证了,而是直接返回未授权,因为基于url的鉴权在上面的hasPermission中完成了。 + if (method.getAnnotation(SaTokenDenyAuth.class) != null) { + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + try { + //执行基于stoken的注解鉴权。 + SaStrategy.instance.checkMethodAnnotation.accept(method); + } catch (SaTokenException e) { + return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.NO_OPERATION_PERMISSION); + } + return ResponseResult.success(); + } + + /** + * 构建satoken的登录Id。 + * + * @return 拼接后的完整登录Id。 + */ + public static String makeLoginId(LoginUserInfo userInfo) { + StringBuilder sb = new StringBuilder(128); + sb.append("SATOKEN_LOGIN:"); + if (userInfo.getTenantId() != null) { + sb.append(userInfo.getTenantId()).append(":"); + } + sb.append(userInfo.getLoginName()).append(":").append(userInfo.getUserId()); + return sb.toString(); + } + + /** + * 获取所有的权限字列表数据。 + * + * @return 所有的权限字列表数据。 + */ + public List getAllPermCodes() { + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + if (!permCodeMap.isExists()) { + return CollUtil.empty(String.class); + } + Set permCodeSet = new TreeSet<>(); + for (RMap.Entry> entry : permCodeMap.entrySet()) { + CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey())); + } + return new LinkedList<>(permCodeSet); + } + + /** + * 获取所有租户运营应用的权限字列表数据。 + * + * @return 所有的权限字列表数据。 + */ + public List getAllTenantPermCodes() { + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + if (!permCodeMap.isExists()) { + return CollUtil.empty(String.class); + } + Set permCodeSet = new TreeSet<>(); + for (RMap.Entry> entry : permCodeMap.entrySet()) { + if (!entry.getKey().equals(ApplicationConstant.TENANT_ADMIN_APP_NAME)) { + CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey())); + } + } + return new LinkedList<>(permCodeSet); + } + + /** + * 获取所有租户管理应用的权限字列表数据。 + * + * @return 所有的权限字列表数据。 + */ + public List getAllTenantAdminPermCodes() { + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + if (!permCodeMap.isExists()) { + return CollUtil.empty(String.class); + } + Set permCodeSet = new TreeSet<>(); + for (RMap.Entry> entry : permCodeMap.entrySet()) { + if (entry.getKey().equals(ApplicationConstant.TENANT_ADMIN_APP_NAME)) { + CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey())); + } + } + return new LinkedList<>(permCodeSet); + } + + /** + * 收集当前服务的SaToken权限字列表,并缓存到Redis,便于统一查询。 + * + * @param event 服务应用的启动事件。 + */ + public void collectPermCodes(ApplicationReadyEvent event) { + redissonClient.getTopic(SA_TOKEN_PERM_CODES_PUBLISH_TOPIC) + .addListener(String.class, (channel, message) -> this.doCollect(event)); + this.doCollect(event); + } + + /** + * 向所有已启动的服务发送权限字同步事件。 + */ + public void publishCollectPermCodes() { + RTopic topic = redissonClient.getTopic(SA_TOKEN_PERM_CODES_PUBLISH_TOPIC); + topic.publish(null); + } + + private void doCollect(ApplicationReadyEvent event) { + Map controllerMap = event.getApplicationContext().getBeansWithAnnotation(RestController.class); + Set permCodes = new HashSet<>(); + for (Map.Entry entry : controllerMap.entrySet()) { + Object targetBean = AopTargetUtil.getTarget(entry.getValue()); + Method[] methods = ReflectUtil.getPublicMethods(targetBean.getClass()); + Arrays.stream(methods) + .map(m -> m.getAnnotation(SaCheckPermission.class)) + .filter(Objects::nonNull) + .forEach(anno -> Collections.addAll(permCodes, anno.value())); + } + RMap> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY); + permCodeMap.put(applicationName, permCodes); + } + + @SuppressWarnings("unchecked") + private boolean hasPermission(String sessionId, String url) { + // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。 + Set localPermSet; + String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL."); + Cache.ValueWrapper wrapper = cache.get(permKey); + if (wrapper == null) { + RSet permSet = redissonClient.getSet(permKey); + localPermSet = permSet.readAll(); + cache.put(permKey, localPermSet); + } else { + localPermSet = (Set) wrapper.get(); + } + return CollUtil.contains(localPermSet, url); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java new file mode 100644 index 00000000..d0339da9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-satoken/src/main/java/com/orangeforms/common/satoken/util/StpInterfaceImpl.java @@ -0,0 +1,62 @@ +package com.orangeforms.common.satoken.util; + +import cn.dev33.satoken.stp.StpInterface; +import com.orangeforms.common.core.cache.CacheConfig; +import com.orangeforms.common.core.object.TokenData; +import com.orangeforms.common.core.util.RedisKeyUtil; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * 自定义权限加载接口实现类 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + @Autowired + private RedissonClient redissonClient; + @Resource(name = "caffeineCacheManager") + private CacheManager cacheManager; + + /** + * 返回一个账号所拥有的权限码集合 + */ + @SuppressWarnings("unchecked") + @Override + public List getPermissionList(Object loginId, String loginType) { + TokenData tokenData = TokenData.takeFromRequest(); + String permCodeKey = RedisKeyUtil.makeSessionPermCodeKey(tokenData.getSessionId()); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERM_CODE_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERM_CODE_CACHE can't be NULL"); + Cache.ValueWrapper wrapper = cache.get(permCodeKey); + if (wrapper != null) { + return (List) wrapper.get(); + } + RSet permCodeSet = redissonClient.getSet(permCodeKey); + Set localPermCodeSet = permCodeSet.readAll(); + List permCodeList = new ArrayList<>(localPermCodeSet); + cache.put(permCodeKey, permCodeList); + return permCodeList; + } + + /** + * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) + */ + @Override + public List getRoleList(Object loginId, String loginType) { + return new ArrayList<>(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-sequence/pom.xml new file mode 100644 index 00000000..36502af3 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/pom.xml @@ -0,0 +1,24 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-sequence + 1.0.0 + common-sequence + jar + + + + com.orangeforms + common-core + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java new file mode 100644 index 00000000..327ce435 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorAutoConfig.java @@ -0,0 +1,14 @@ +package com.orangeforms.common.sequence.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * common-sequence模块的自动配置引导类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties({IdGeneratorProperties.class}) +public class IdGeneratorAutoConfig { + +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java new file mode 100644 index 00000000..f20076d8 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/config/IdGeneratorProperties.java @@ -0,0 +1,20 @@ +package com.orangeforms.common.sequence.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common-sequence模块的配置类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties(prefix = "common-sequence") +public class IdGeneratorProperties { + + /** + * 基础版生成器所需的WorkNode参数值。仅当advanceIdGenerator为false时生效。 + */ + private Integer snowflakeWorkNode = 1; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java new file mode 100644 index 00000000..fccf75de --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/BasicIdGenerator.java @@ -0,0 +1,47 @@ +package com.orangeforms.common.sequence.generator; + +import cn.hutool.core.lang.Snowflake; + +/** + * 基础版snowflake计算工具类。 + * 和SnowflakeIdGenerator相比,相同点是均为基于Snowflake算法的生成器。不同点在于当前类的 + * WorkNodeId是通过配置文件静态指定的。而SnowflakeIdGenerator的WorkNodeId是由zk生成的。 + * + * @author Jerry + * @date 2024-07-02 + */ +public class BasicIdGenerator implements MyIdGenerator { + + private final Snowflake snowflake; + + /** + * 构造函数。 + * + * @param workNode 工作节点。 + */ + public BasicIdGenerator(Integer workNode) { + snowflake = new Snowflake(workNode, 0); + } + + /** + * 获取基于Snowflake算法的数值型Id。 + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + @Override + public long nextLongId() { + return this.snowflake.nextId(); + } + + /** + * 获取基于Snowflake算法的字符串Id。 + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + @Override + public String nextStringId() { + return this.snowflake.nextIdStr(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java new file mode 100644 index 00000000..209d3c8e --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/generator/MyIdGenerator.java @@ -0,0 +1,24 @@ +package com.orangeforms.common.sequence.generator; + +/** + * 分布式Id生成器的统一接口。 + * + * @author Jerry + * @date 2024-07-02 + */ +public interface MyIdGenerator { + + /** + * 获取数值型分布式Id。 + * + * @return 生成后的Id。 + */ + long nextLongId(); + + /** + * 获取字符型分布式Id。 + * + * @return 生成后的Id。 + */ + String nextStringId(); +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java new file mode 100644 index 00000000..441ba9d9 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/java/com/orangeforms/common/sequence/wrapper/IdGeneratorWrapper.java @@ -0,0 +1,52 @@ +package com.orangeforms.common.sequence.wrapper; + +import com.orangeforms.common.sequence.config.IdGeneratorProperties; +import com.orangeforms.common.sequence.generator.BasicIdGenerator; +import com.orangeforms.common.sequence.generator.MyIdGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +/** + * 分布式Id生成器的封装类。该对象可根据配置选择不同的生成器实现类。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Component +public class IdGeneratorWrapper { + + @Autowired + private IdGeneratorProperties properties; + /** + * Id生成器接口对象。 + */ + private MyIdGenerator idGenerator; + + /** + * 今后如果支持更多Id生成器时,可以在该函数内实现不同生成器的动态选择。 + */ + @PostConstruct + public void init() { + idGenerator = new BasicIdGenerator(properties.getSnowflakeWorkNode()); + } + + /** + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + public long nextLongId() { + return idGenerator.nextLongId(); + } + + /** + * 由于底层实现为synchronized方法,因此计算过程串行化,且线程安全。 + * + * @return 计算后的全局唯一Id。 + */ + public String nextStringId() { + return idGenerator.nextStringId(); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..f917b714 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-sequence/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.sequence.config.IdGeneratorAutoConfig \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-swagger/pom.xml b/OrangeFormsOpen-MybatisPlus/common/common-swagger/pom.xml new file mode 100644 index 00000000..683c9952 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-swagger/pom.xml @@ -0,0 +1,40 @@ + + + + common + com.orangeforms + 1.0.0 + + 4.0.0 + + common-swagger + 1.0.0 + common-swagger + jar + + + + + com.github.xiaoymin + knife4j-dependencies + ${knife4j.version} + pom + import + + + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + com.orangeforms + common-core + 1.0.0 + + + \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java new file mode 100644 index 00000000..1ad2a2ae --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerAutoConfiguration.java @@ -0,0 +1,70 @@ +package com.orangeforms.common.swagger.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 自动加载bean的配置对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@EnableConfigurationProperties(SwaggerProperties.class) +@ConditionalOnProperty(prefix = "common-swagger", name = "enabled") +public class SwaggerAutoConfiguration { + + @Bean + public GroupedOpenApi upmsApi(SwaggerProperties p) { + String[] paths = {"/admin/upms/**"}; + String[] packagedToMatch = {p.getServiceBasePackage() + ".upms.controller"}; + return GroupedOpenApi.builder().group("用户权限分组接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi bizApi(SwaggerProperties p) { + String[] paths = {"/admin/app/**"}; + String[] packagedToMatch = {p.getServiceBasePackage() + ".app.controller"}; + return GroupedOpenApi.builder().group("业务应用分组接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi workflowApi(SwaggerProperties p) { + String[] paths = {"/admin/flow/**"}; + String[] packagedToMatch = {p.getBasePackage() + ".common.flow.controller"}; + return GroupedOpenApi.builder().group("工作流通用操作接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi onlineApi(SwaggerProperties p) { + String[] paths = {"/admin/online/**"}; + String[] packagedToMatch = {p.getBasePackage() + ".common.online.controller"}; + return GroupedOpenApi.builder().group("在线表单操作接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public GroupedOpenApi reportApi(SwaggerProperties p) { + String[] paths = {"/admin/report/**"}; + String[] packagedToMatch = {p.getBasePackage() + ".common.report.controller"}; + return GroupedOpenApi.builder().group("报表打印操作接口") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public OpenAPI customOpenApi(SwaggerProperties p) { + Info info = new Info().title(p.getTitle()).version(p.getVersion()).description(p.getDescription()); + return new OpenAPI().info(info); + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java new file mode 100644 index 00000000..7f84999f --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/config/SwaggerProperties.java @@ -0,0 +1,45 @@ +package com.orangeforms.common.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 配置参数对象。 + * + * @author Jerry + * @date 2024-07-02 + */ +@Data +@ConfigurationProperties("common-swagger") +public class SwaggerProperties { + + /** + * 是否开启Swagger。 + */ + private Boolean enabled; + + /** + * Swagger解析的基础包路径。 + **/ + private String basePackage = ""; + + /** + * Swagger解析的服务包路径。 + **/ + private String serviceBasePackage = ""; + + /** + * ApiInfo中的标题。 + **/ + private String title = ""; + + /** + * ApiInfo中的描述信息。 + **/ + private String description = ""; + + /** + * ApiInfo中的版本信息。 + **/ + private String version = ""; +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java new file mode 100644 index 00000000..4bba5b3b --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/java/com/orangeforms/common/swagger/plugin/MyGlobalOperationCustomer.java @@ -0,0 +1,194 @@ +package com.orangeforms.common.swagger.plugin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.orangeforms.common.core.annotation.MyRequestBody; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.models.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.util.*; +import java.util.stream.Stream; + +/** + * @author xiaoymin@foxmail.com + */ +@Slf4j +@Component +public class MyGlobalOperationCustomer implements GlobalOperationCustomizer { + + /** + * 注解包路径名称 + */ + private static final String REF_KEY = "$ref"; + private static final String REF_SCHEMA_PREFIX = "#/components/schemas/"; + private final Map, Set> cacheClassProperties = MapUtil.newHashMap(); + private static final String EXTENSION_ORANGE_FORM_NAME = "x-orangeforms"; + private static final String EXTENSION_ORANGE_FORM_IGNORE_NAME = "x-orangeforms-ignore-parameters"; + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + this.handleSummary(operation, handlerMethod); + if (handlerMethod.getMethod().getParameterCount() <= 0) { + return operation; + } + Parameter[] parameters = handlerMethod.getMethod().getParameters(); + if (ArrayUtil.isEmpty(parameters)) { + return operation; + } + Map properties = MapUtil.newHashMap(); + Map extensions = MapUtil.newHashMap(); + Set ignoreFieldName = CollUtil.newHashSet(); + List required = new ArrayList<>(); + Map paramMap = getParameterDescription(handlerMethod.getMethod()); + for (Parameter parameter : parameters) { + Annotation[] annos = parameter.getAnnotations(); + if (ArrayUtil.isEmpty(annos)) { + continue; + } + long count = Stream.of(annos).filter(anno -> anno.annotationType().equals(MyRequestBody.class)).count(); + if (count > 0) { + this.handleParameterDetail(parameter, properties, paramMap, ignoreFieldName, required); + } + } + if (!properties.isEmpty()) { + extensions.put("properties", properties); + extensions.put("type", "object"); + //required字段 + if (!required.isEmpty()) { + extensions.put("required", required); + } + String generateSchemaName = handlerMethod.getMethod().getName() + "DynamicReq"; + Map orangeExtensions = MapUtil.newHashMap(); + orangeExtensions.put(generateSchemaName, extensions); + //增加扩展属性 + operation.addExtension(EXTENSION_ORANGE_FORM_NAME, orangeExtensions); + if (!ignoreFieldName.isEmpty()) { + operation.addExtension(EXTENSION_ORANGE_FORM_IGNORE_NAME, ignoreFieldName); + } + } + return operation; + } + + private void handleSummary(Operation operation, HandlerMethod handlerMethod) { + io.swagger.v3.oas.annotations.Operation operationAnno = + handlerMethod.getMethod().getAnnotation(io.swagger.v3.oas.annotations.Operation.class); + if (operationAnno == null || StrUtil.isBlank(operationAnno.summary())) { + operation.setSummary(handlerMethod.getMethod().getName()); + } + } + + private void handleParameterDetail( + Parameter parameter, + Map properties, + Map paramMap, + Set ignoreFieldName, + List required) { + Class parameterType = parameter.getType(); + String schemaName = parameterType.getSimpleName(); + //添加忽律参数名称 + ignoreFieldName.addAll(getClassFields(parameterType)); + //处理schema注解别名的情况 + Schema schema = parameterType.getAnnotation(Schema.class); + if (schema != null && StrUtil.isNotBlank(schema.name())) { + schemaName = schema.name(); + } + Map value = MapUtil.newHashMap(); + //此处需要判断parameter的基础数据类型 + if (parameterType.isPrimitive() || parameterType.getName().startsWith("java.lang")) { + //基础数据类型 + ignoreFieldName.add(parameter.getName()); + value.put("type", parameterType.getSimpleName().toLowerCase()); + //判断format + } else if (Collection.class.isAssignableFrom(parameterType)) { + //集合类型 + value.put("type", "array"); + //获取泛型 + getGenericType(parameterType, parameter.getParameterizedType()) + .ifPresent(s -> value.put("items", MapUtil.builder(REF_KEY, REF_SCHEMA_PREFIX + s).build())); + } else { + //引用类型 + value.put(REF_KEY, REF_SCHEMA_PREFIX + schemaName); + } + //补一个description + io.swagger.v3.oas.annotations.Parameter paramAnnotation = paramMap.get(parameter.getName()); + if (paramAnnotation != null) { + //忽略该参数 + ignoreFieldName.add(paramAnnotation.name()); + value.put("description", paramAnnotation.description()); + if (StrUtil.isNotBlank(paramAnnotation.example())) { + value.put("default", paramAnnotation.example()); + } + // required参数 + if (paramAnnotation.required()) { + required.add(parameter.getName()); + } + } + properties.put(parameter.getName(), value); + } + + private Optional getGenericType(Class clazz, Type type) { + Type genericSuperclass = clazz.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType || type instanceof ParameterizedType) { + if (type instanceof ParameterizedType) { + genericSuperclass = type; + } + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + return Optional.of(((Class) actualTypeArguments[0]).getSimpleName()); + } + return Optional.empty(); + } + + private Set getClassFields(Class parameterType) { + if (parameterType == null) { + return Collections.emptySet(); + } + if (cacheClassProperties.containsKey(parameterType)) { + return cacheClassProperties.get(parameterType); + } + Set fieldNames = new HashSet<>(); + try { + Field[] fields = parameterType.getDeclaredFields(); + if (fields.length > 0) { + for (Field field : fields) { + fieldNames.add(field.getName()); + } + cacheClassProperties.put(parameterType, fieldNames); + return fieldNames; + } + } catch (Exception e) { + //ignore + } + return Collections.emptySet(); + } + + private Map getParameterDescription(Method method) { + Parameters parameters = method.getAnnotation(Parameters.class); + Map resultMap = MapUtil.newHashMap(); + if (parameters != null) { + io.swagger.v3.oas.annotations.Parameter[] parameters1 = parameters.value(); + if (parameters1 != null && parameters1.length > 0) { + for (io.swagger.v3.oas.annotations.Parameter parameter : parameters1) { + resultMap.put(parameter.name(), parameter); + } + return resultMap; + } + } else { + io.swagger.v3.oas.annotations.Parameter parameter = + method.getAnnotation(io.swagger.v3.oas.annotations.Parameter.class); + if (parameter != null) { + resultMap.put(parameter.name(), parameter); + } + } + return resultMap; + } +} diff --git a/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..b94a3251 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orangeforms.common.swagger.config.SwaggerAutoConfiguration \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/common/pom.xml b/OrangeFormsOpen-MybatisPlus/common/pom.xml new file mode 100644 index 00000000..9ba52d48 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/common/pom.xml @@ -0,0 +1,30 @@ + + + + com.orangeforms + OrangeFormsOpen + 1.0.0 + + 4.0.0 + + common + pom + + + common-dbutil + common-ext + common-core + common-log + common-dict + common-datafilter + common-satoken + common-online + common-flow-online + common-flow + common-redis + common-minio + common-sequence + common-swagger + + diff --git a/OrangeFormsOpen-MybatisPlus/pom.xml b/OrangeFormsOpen-MybatisPlus/pom.xml new file mode 100644 index 00000000..cead7df2 --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/pom.xml @@ -0,0 +1,171 @@ + + + 4.0.0 + + com.orangeforms + OrangeFormsOpen + 1.0.0 + OrangeFormsOpen + pom + + + 3.1.6 + 3.1.6 + UTF-8 + 17 + 17 + 17 + OrangeFormsOpen + + 2.10.13 + 20.0 + 2.6 + 4.4 + 1.8 + 5.2.2 + 5.0.0 + 5.8.23 + 0.12.3 + 1.2.83 + 1.1.5 + 2.9.3 + 1.18.20 + 8.0.1.Final + 7.0.1 + 3.15.4 + 8.4.5 + 2.0.0 + 4.5.0 + + 1.2.16 + 3.5.4.1 + 1.4.7 + + + + application-webadmin + common + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-logging + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-cache + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.security + spring-security-crypto + + + + org.springframework.boot + spring-boot-starter-actuator + + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + mysql + mysql-connector-java + 8.0.22 + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + src/main/resources + + **/*.* + + false + + + src/main/java + + **/*.xml + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + -parameters + + ${maven.compiler.target} + ${maven.compiler.source} + UTF-8 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/OrangeFormsOpen-MybatisPlus/zz-resource/.DS_Store b/OrangeFormsOpen-MybatisPlus/zz-resource/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..22e57237677b48be21a85b06fd0c4b16d37fd3a3 GIT binary patch literal 6148 zcmeHKO-lnY5S`H$N-ff(h~oZ%g7wmihqbJ~pa-wo){o+rE!N%_|C^rtMV@?<8SE}~ z%brBa3}jw5nMqzAB-s!V4_DJZQJ08XD1wbP+%G2mQZ{VQS~@@uV@&9UX0)JJT5d#p zgDc<){5J(;@3v`5W9nhg7WQxY*dI-@`63W*9^O>N&9fvKr`Z_&t+)5D!Rg7zRdwvN zcI@J*IDdp8sRN!FI6l)o&Y2!P;H*heUzWHsUv7&n@@;-z%#3g4*z;`7xlVkEH=ir- z70$#I-0!f0D&7()YpjSZ@+Qg$XU9S3E%9sKb>d6Bm5ck`RseUlSnEL1y({1fxB@>4 z$oUYV2*!rFVmLbB!V>`4KyxtE)31tiZm0cIE!Ru73ZQll;pSa0PZs0d6FtWQZl%-MXxguI%{v!}%aOVpAsRG}}B6;-y literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-MybatisPlus/zz-resource/db-scripts/.DS_Store b/OrangeFormsOpen-MybatisPlus/zz-resource/db-scripts/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0\n\n \n \n \n \n \n \n \n Flow_0d86buw\n \n \n \n \n \n Flow_1bxwcza\n \n \n \n \n \n \n \n \n \n \n Flow_0d86buw\n Flow_1u40dt7\n \n \n \n \n \n \n \n \n \n \n Flow_1u40dt7\n Flow_05s1j0n\n \n \n \n \n \n \n \n \n \n \n Flow_05s1j0n\n Flow_1bxwcza\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n', 0, 0, 1809132177523216384, 1809132635633487872, NULL, '{\"middle\":\"DD\",\"idWidth\":5,\"prefix\":\"LL\",\"precisionTo\":\"DAYS\",\"calculateWhenView\":true}', '{\"approvalStatusDict\":[{\"id\":1,\"name\":\"同意\",\"_X_ROW_KEY\":\"row_57\"},{\"id\":2,\"name\":\"拒绝\",\"_X_ROW_KEY\":\"row_58\"},{\"id\":3,\"name\":\"驳回\",\"_X_ROW_KEY\":\"row_59\"},{\"id\":4,\"name\":\"会签同意\",\"_X_ROW_KEY\":\"row_60\"},{\"id\":5,\"name\":\"会签拒绝\",\"_X_ROW_KEY\":\"row_61\"}],\"notifyTypes\":[\"email\"],\"cascadeDeleteBusinessData\":true,\"supportRevive\":false}', '2024-07-05 16:36:39', 1808020007993479168, '2024-07-05 16:35:15', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_entry_publish +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_entry_publish`; +CREATE TABLE `zz_flow_entry_publish` ( + `entry_publish_id` bigint NOT NULL COMMENT '主键Id', + `entry_id` bigint NOT NULL COMMENT '流程Id', + `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的定义Id', + `deploy_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的部署Id', + `publish_version` int NOT NULL COMMENT '发布版本', + `active_status` bit(1) NOT NULL COMMENT '激活状态', + `main_version` bit(1) NOT NULL COMMENT '是否为主版本', + `extension_data` varchar(3000) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程的自定义扩展数据', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `publish_time` datetime NOT NULL COMMENT '发布时间', + `init_task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '第一个非开始节点任务的附加信息', + `analyzed_node_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '分析后的节点JSON信息', + PRIMARY KEY (`entry_publish_id`) USING BTREE, + UNIQUE KEY `uk_process_definition_id` (`process_definition_id`) USING BTREE, + KEY `idx_entry_id` (`entry_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程发布表'; + +-- ---------------------------- +-- Records of zz_flow_entry_publish +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_entry_publish` VALUES (1809144428770627584, 1809143991627681792, 'flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'bcd05b06-3aa9-11ef-86ec-acde48001122', 1, b'1', b'1', '{\"approvalStatusDict\":[{\"id\":1,\"name\":\"同意\",\"_X_ROW_KEY\":\"row_57\"},{\"id\":2,\"name\":\"拒绝\",\"_X_ROW_KEY\":\"row_58\"},{\"id\":3,\"name\":\"驳回\",\"_X_ROW_KEY\":\"row_59\"},{\"id\":4,\"name\":\"会签同意\",\"_X_ROW_KEY\":\"row_60\"},{\"id\":5,\"name\":\"会签拒绝\",\"_X_ROW_KEY\":\"row_61\"}],\"notifyTypes\":[\"email\"],\"cascadeDeleteBusinessData\":true,\"supportRevive\":false}', 1808020007993479168, '2024-07-05 16:36:59', '{\"assignee\":\"${startUserName}\",\"formId\":1809132635633487872,\"groupType\":\"ASSIGNEE\",\"operationList\":[{\"showOrder\":\"0\",\"id\":\"1720168540672\",\"label\":\"同意\",\"type\":\"agree\"}],\"readOnly\":false,\"taskKey\":\"Activity_0vjtv0p\",\"taskType\":1,\"variableList\":[]}', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_entry_publish_variable +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_entry_publish_variable`; +CREATE TABLE `zz_flow_entry_publish_variable` ( + `variable_id` bigint NOT NULL COMMENT '主键Id', + `entry_publish_id` bigint NOT NULL COMMENT '流程Id', + `variable_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '变量名', + `show_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '显示名', + `variable_type` int NOT NULL COMMENT '变量类型', + `bind_datasource_id` bigint DEFAULT NULL COMMENT '绑定数据源Id', + `bind_relation_id` bigint DEFAULT NULL COMMENT '绑定数据源关联Id', + `bind_column_id` bigint DEFAULT NULL COMMENT '绑定字段Id', + `builtin` bit(1) NOT NULL COMMENT '是否内置', + PRIMARY KEY (`variable_id`) USING BTREE, + KEY `idx_entry_publish_id` (`entry_publish_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程发布变量表'; + +-- ---------------------------- +-- Records of zz_flow_entry_publish_variable +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_entry_publish_variable` VALUES (1809144430116999168, 1809144428770627584, 'operationType', '审批类型', 1, NULL, NULL, NULL, b'1'); +INSERT INTO `zz_flow_entry_publish_variable` VALUES (1809144430116999169, 1809144428770627584, 'startUserName', '流程启动用户', 0, NULL, NULL, NULL, b'1'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_entry_variable +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_entry_variable`; +CREATE TABLE `zz_flow_entry_variable` ( + `variable_id` bigint NOT NULL COMMENT '主键Id', + `entry_id` bigint NOT NULL COMMENT '流程Id', + `variable_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '变量名', + `show_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '显示名', + `variable_type` int NOT NULL COMMENT '变量类型', + `bind_datasource_id` bigint DEFAULT NULL COMMENT '绑定数据源Id', + `bind_relation_id` bigint DEFAULT NULL COMMENT '绑定数据源关联Id', + `bind_column_id` bigint DEFAULT NULL COMMENT '绑定字段Id', + `builtin` bit(1) NOT NULL COMMENT '是否内置', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`variable_id`) USING BTREE, + UNIQUE KEY `uk_entry_id_variable_name` (`entry_id`,`variable_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程变量表'; + +-- ---------------------------- +-- Records of zz_flow_entry_variable +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_entry_variable` VALUES (1809143992151969793, 1809143991627681792, 'operationType', '审批类型', 1, NULL, NULL, NULL, b'1', '2024-07-05 16:35:15'); +INSERT INTO `zz_flow_entry_variable` VALUES (1809143992630120448, 1809143991627681792, 'startUserName', '流程启动用户', 0, NULL, NULL, NULL, b'1', '2024-07-05 16:35:15'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_message +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_message`; +CREATE TABLE `zz_flow_message` ( + `message_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用Id', + `message_type` tinyint NOT NULL COMMENT '消息类型', + `message_content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '消息内容', + `remind_count` int DEFAULT '0' COMMENT '催办次数', + `work_order_id` bigint DEFAULT NULL COMMENT '工单Id', + `process_definition_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义Id', + `process_definition_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义标识', + `process_definition_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义名称', + `process_instance_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程实例Id', + `process_instance_initiator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程实例发起者', + `task_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程任务Id', + `task_definition_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程任务定义标识', + `task_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程任务名称', + `task_start_time` datetime DEFAULT NULL COMMENT '任务开始时间', + `task_finished` bit(1) NOT NULL DEFAULT b'0' COMMENT '任务是否已完成', + `task_assignee` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务指派人登录名', + `business_data_shot` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '业务数据快照', + `online_form_data` bit(1) DEFAULT NULL COMMENT '是否为在线表单消息数据', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者显示名', + PRIMARY KEY (`message_id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE, + KEY `idx_notified_username` (`task_assignee`) USING BTREE, + KEY `idx_process_instance_id` (`process_instance_id`) USING BTREE, + KEY `idx_message_type` (`message_type`) USING BTREE, + KEY `idx_task_id` (`task_id`) USING BTREE, + KEY `idx_task_finished` (`task_finished`) USING BTREE, + KEY `idx_update_time` (`update_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程消息通知表'; + +-- ---------------------------- +-- Table structure for zz_flow_msg_candidate_identity +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_msg_candidate_identity`; +CREATE TABLE `zz_flow_msg_candidate_identity` ( + `id` bigint NOT NULL COMMENT '主键Id', + `message_id` bigint NOT NULL COMMENT '流程任务Id', + `candidate_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '候选身份类型', + `candidate_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '候选身份Id', + PRIMARY KEY (`id`), + KEY `idx_candidate_id` (`candidate_id`) USING BTREE, + KEY `idx_message_id` (`message_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程消息通知候选人表'; + +-- ---------------------------- +-- Table structure for zz_flow_msg_identity_operation +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_msg_identity_operation`; +CREATE TABLE `zz_flow_msg_identity_operation` ( + `id` bigint NOT NULL COMMENT '主键Id', + `message_id` bigint NOT NULL COMMENT '流程任务Id', + `login_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户登录名', + `operation_type` int NOT NULL COMMENT '操作类型', + `operation_time` datetime NOT NULL COMMENT '操作时间', + PRIMARY KEY (`id`), + KEY `idx_message_id` (`message_id`) USING BTREE, + KEY `idx_login_name` (`login_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程消息候选人操作表'; + +-- ---------------------------- +-- Table structure for zz_flow_multi_instance_trans +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_multi_instance_trans`; +CREATE TABLE `zz_flow_multi_instance_trans` ( + `id` bigint NOT NULL COMMENT '主键Id', + `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程实例Id', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务Id', + `task_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务标识', + `multi_instance_exec_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '会签任务的执行Id', + `execution_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务的执行Id', + `assignee_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '会签指派人列表', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_login_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者登录名', + `create_username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者用户名', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_execution_id_task_id` (`execution_id`,`task_id`) USING BTREE, + KEY `idx_multi_instance_exec_id` (`multi_instance_exec_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程多实例任务审批流水表'; + +-- ---------------------------- +-- Table structure for zz_flow_task_comment +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_task_comment`; +CREATE TABLE `zz_flow_task_comment` ( + `id` bigint NOT NULL COMMENT '主键Id', + `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程实例Id', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务Id', + `task_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务标识', + `task_name` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务名称', + `target_task_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '目标任务标识', + `execution_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务的执行Id', + `multi_instance_exec_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '会签任务的执行Id', + `approval_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '审批类型', + `task_comment` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '批注内容', + `delegate_assignee` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '委托指定人,比如加签、转办等', + `custom_business_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '自定义数据。开发者可自行扩展,推荐使用JSON格式数据', + `head_image_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '审批用户头像', + `create_user_id` bigint DEFAULT NULL COMMENT '创建者Id', + `create_login_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建者登录名', + `create_username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建者用户名', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_multi_instance_exec_id` (`multi_instance_exec_id`) USING BTREE, + KEY `idx_process_instance_id` (`process_instance_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程任务审批表'; + +-- ---------------------------- +-- Records of zz_flow_task_comment +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_task_comment` VALUES (1809146487481831424, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'e200f745-3aaa-11ef-86ec-acde48001122', 'Activity_0vjtv0p', '录入', NULL, 'e1fbee31-3aaa-11ef-86ec-acde48001122', NULL, 'agree', NULL, NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:45:10'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146598064656384, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'e322e20d-3aaa-11ef-86ec-acde48001122', 'Activity_06g14pf', '审批A', NULL, 'e1fbee31-3aaa-11ef-86ec-acde48001122', NULL, 'agree', '11', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:45:36'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146699361292288, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'f2dcc311-3aaa-11ef-86ec-acde48001122', 'Activity_0dn7u52', '审批B', NULL, NULL, NULL, 'reject', '11', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:46:00'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146743762194432, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 'ffef78e6-3aaa-11ef-86ec-acde48001122', 'Activity_06g14pf', '审批A', NULL, NULL, NULL, 'reject', '33', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:46:11'); +INSERT INTO `zz_flow_task_comment` VALUES (1809146774330281984, 'e1fb2ada-3aaa-11ef-86ec-acde48001122', '0669cc2a-3aab-11ef-86ec-acde48001122', 'Activity_0vjtv0p', '录入', NULL, '0669cc28-3aab-11ef-86ec-acde48001122', NULL, 'agree', '44', NULL, NULL, NULL, 1808020007993479168, 'admin', '管理员', '2024-07-05 16:46:18'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_task_ext +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_task_ext`; +CREATE TABLE `zz_flow_task_ext` ( + `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的定义Id', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎任务Id', + `operation_list_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '操作列表JSON', + `variable_list_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '变量列表JSON', + `assignee_list_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '存储多实例的assigneeList的JSON', + `group_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '分组类型', + `dept_post_list_json` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存岗位相关的数据', + `role_ids` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存角色Id数据', + `dept_ids` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存部门Id数据', + `candidate_usernames` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '保存候选组用户名数据', + `copy_list_json` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '抄送相关的数据', + `extra_data_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '用户任务的扩展属性,存储为JSON的字符串格式', + PRIMARY KEY (`process_definition_id`,`task_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程流程图任务扩展表'; + +-- ---------------------------- +-- Records of zz_flow_task_ext +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_task_ext` VALUES ('flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'Activity_06g14pf', '[{\"showOrder\":\"0\",\"id\":\"1720168555059\",\"label\":\"同意\",\"type\":\"agree\"},{\"showOrder\":\"0\",\"id\":\"1720168558485\",\"label\":\"驳回到起点\",\"type\":\"rejectToStart\"}]', NULL, NULL, 'ASSIGNEE', '[]', NULL, NULL, NULL, '[]', '{\"flowNotifyTypeList\":[\"email\"]}'); +INSERT INTO `zz_flow_task_ext` VALUES ('flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'Activity_0dn7u52', '[{\"showOrder\":\"0\",\"id\":\"1720168573903\",\"label\":\"同意\",\"type\":\"agree\"},{\"showOrder\":\"0\",\"id\":\"1720168577495\",\"label\":\"驳回\",\"type\":\"reject\"}]', NULL, NULL, 'ASSIGNEE', '[]', NULL, NULL, NULL, '[]', '{\"flowNotifyTypeList\":[\"email\"]}'); +INSERT INTO `zz_flow_task_ext` VALUES ('flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'Activity_0vjtv0p', '[{\"showOrder\":\"0\",\"id\":\"1720168540672\",\"label\":\"同意\",\"type\":\"agree\"}]', NULL, NULL, 'ASSIGNEE', '[]', NULL, NULL, NULL, '[]', '{\"flowNotifyTypeList\":[\"email\"]}'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_work_order +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_work_order`; +CREATE TABLE `zz_flow_work_order` ( + `work_order_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `work_order_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '工单编码字段', + `process_definition_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程定义标识', + `process_definition_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '流程名称', + `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程引擎的定义Id', + `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '流程实例Id', + `online_table_id` bigint DEFAULT NULL COMMENT '在线表单的主表Id', + `table_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用于静态表单的表名', + `business_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '业务主键值', + `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '未完成的任务Id', + `task_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '未完成的任务名称', + `task_definition_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '未完成的任务标识', + `latest_approval_status` int DEFAULT NULL COMMENT '最近的审批状态', + `flow_status` int NOT NULL DEFAULT '0' COMMENT '流程状态', + `submit_username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '提交用户登录名称', + `dept_id` bigint NOT NULL COMMENT '提交用户所在部门Id', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`work_order_id`) USING BTREE, + UNIQUE KEY `uk_process_instance_id` (`process_instance_id`) USING BTREE, + UNIQUE KEY `uk_work_order_code` (`work_order_code`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE, + KEY `idx_process_definition_key` (`process_definition_key`) USING BTREE, + KEY `idx_create_user_id` (`create_user_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE, + KEY `idx_dept_id` (`dept_id`) USING BTREE, + KEY `idx_table_name` (`table_name`) USING BTREE, + KEY `idx_business_key` (`business_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程工单表'; + +-- ---------------------------- +-- Records of zz_flow_work_order +-- ---------------------------- +BEGIN; +INSERT INTO `zz_flow_work_order` VALUES (1809146486244511744, NULL, NULL, 'LL20240705DD00001', 'flowLeave', '请假申请', 'flowLeave:1:be0642f9-3aa9-11ef-86ec-acde48001122', 'e1fb2ada-3aaa-11ef-86ec-acde48001122', 1809132251556876288, NULL, '1809146480452177920', NULL, NULL, NULL, NULL, 1, 'admin', 1808020008341606402, '2024-07-05 16:46:18', 1808020007993479168, '2024-07-05 16:45:09', 1808020007993479168, 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_flow_work_order_ext +-- ---------------------------- +DROP TABLE IF EXISTS `zz_flow_work_order_ext`; +CREATE TABLE `zz_flow_work_order_ext` ( + `id` bigint NOT NULL COMMENT '主键Id', + `work_order_id` bigint NOT NULL COMMENT '工单Id', + `draft_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '草稿数据', + `business_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '业务数据', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_work_order_id` (`work_order_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='流程工单扩展表'; + +-- ---------------------------- +-- Table structure for zz_global_dict +-- ---------------------------- +DROP TABLE IF EXISTS `zz_global_dict`; +CREATE TABLE `zz_global_dict` ( + `dict_id` bigint NOT NULL COMMENT '主键Id', + `dict_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典编码', + `dict_name` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典中文名称', + `create_user_id` bigint NOT NULL COMMENT '创建用户Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新用户名', + `update_time` datetime NOT NULL COMMENT '更新时间', + `deleted_flag` int NOT NULL COMMENT '逻辑删除字段', + PRIMARY KEY (`dict_id`), + KEY `idx_dict_code` (`dict_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='全局字典表'; + +-- ---------------------------- +-- Table structure for zz_global_dict_item +-- ---------------------------- +DROP TABLE IF EXISTS `zz_global_dict_item`; +CREATE TABLE `zz_global_dict_item` ( + `id` bigint NOT NULL COMMENT '主键Id', + `dict_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典编码', + `item_id` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '字典数据项Id', + `item_name` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典数据项名称', + `show_order` int NOT NULL COMMENT '显示顺序', + `status` int NOT NULL COMMENT '字典状态', + `create_user_id` bigint NOT NULL COMMENT '创建用户Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新用户名', + `update_time` datetime NOT NULL COMMENT '更新时间', + `deleted_flag` int NOT NULL COMMENT '逻辑删除字段', + PRIMARY KEY (`id`), + KEY `idx_show_order` (`show_order`) USING BTREE, + KEY `idx_dict_code_item_id` (`dict_code`,`item_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='全局字典项目表'; + +-- ---------------------------- +-- Table structure for zz_online_column +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_column`; +CREATE TABLE `zz_online_column` ( + `column_id` bigint NOT NULL COMMENT '主键Id', + `column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字段名', + `table_id` bigint NOT NULL COMMENT '数据表Id', + `column_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '数据表中的字段类型', + `full_column_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '数据表中的完整字段类型(包括了精度和刻度)', + `primary_key` bit(1) NOT NULL COMMENT '是否为主键', + `auto_incr` bit(1) NOT NULL COMMENT '是否是自增主键(0: 不是 1: 是)', + `nullable` bit(1) NOT NULL COMMENT '是否可以为空 (0: 不可以为空 1: 可以为空)', + `column_default` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '缺省值', + `column_show_order` int NOT NULL COMMENT '字段在数据表中的显示位置', + `column_comment` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '数据表中的字段注释', + `object_field_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '对象映射字段名称', + `object_field_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '对象映射字段类型', + `numeric_precision` int DEFAULT NULL COMMENT '数值型字段的精度', + `numeric_scale` int DEFAULT NULL COMMENT '数值型字段的刻度', + `filter_type` int NOT NULL DEFAULT '1' COMMENT '字段过滤类型', + `parent_key` bit(1) NOT NULL COMMENT '是否是主键的父Id', + `dept_filter` bit(1) NOT NULL COMMENT '是否部门过滤字段', + `user_filter` bit(1) NOT NULL COMMENT '是否用户过滤字段', + `field_kind` int DEFAULT NULL COMMENT '字段类别', + `max_file_count` int DEFAULT NULL COMMENT '包含的文件文件数量,0表示无限制', + `upload_file_system_type` int DEFAULT '0' COMMENT '上传文件系统类型', + `encoded_rule` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '编码规则的JSON格式数据', + `mask_field_type` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '脱敏字段类型', + `dict_id` bigint DEFAULT NULL COMMENT '字典Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`column_id`), + KEY `idx_table_id` (`table_id`) USING BTREE, + KEY `idx_dict_id` (`dict_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字段表'; + +-- ---------------------------- +-- Records of zz_online_column +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_column` VALUES (1809132252005666816, 'id', 1809132251556876288, 'bigint', 'bigint', b'1', b'0', b'0', NULL, 1, '主键Id', 'id', 'Long', 19, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:48:36', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132252425097216, 'user_id', 1809132251556876288, 'bigint', 'bigint', b'0', b'0', b'0', NULL, 2, '请假用户', 'userId', 'Long', 19, NULL, 0, b'0', b'0', b'0', 21, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:48:47', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132252852916224, 'leave_reason', 1809132251556876288, 'varchar', 'varchar(512)', b'0', b'0', b'0', NULL, 3, '请假原因', 'leaveReason', 'String', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:47', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132253377204224, 'leave_type', 1809132251556876288, 'int', 'int', b'0', b'0', b'0', NULL, 4, '请假类型', 'leaveType', 'Integer', 10, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:44', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132253733720064, 'leave_begin_time', 1809132251556876288, 'datetime', 'datetime', b'0', b'0', b'0', NULL, 5, '开始时间', 'leaveBeginTime', 'Date', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:50', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132254102818816, 'leave_end_time', 1809132251556876288, 'datetime', 'datetime', b'0', b'0', b'0', NULL, 6, '结束时间', 'leaveEndTime', 'Date', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:54', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132254388031488, 'apply_time', 1809132251556876288, 'datetime', 'datetime', b'0', b'0', b'0', NULL, 7, '申请时间', 'applyTime', 'Date', NULL, NULL, 0, b'0', b'0', b'0', 20, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:57', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132254782296064, 'approval_status', 1809132251556876288, 'int', 'int', b'0', b'0', b'1', NULL, 8, '最后审批状态', 'approvalStatus', 'Integer', 10, NULL, 0, b'0', b'0', b'0', 26, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:53:59', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132255327555584, 'flow_status', 1809132251556876288, 'int', 'int', b'0', b'0', b'1', NULL, 9, '流程状态', 'flowStatus', 'Integer', 10, NULL, 0, b'0', b'0', b'0', 25, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:49:44', 1808020007993479168); +INSERT INTO `zz_online_column` VALUES (1809132255679877120, 'username', 1809132251556876288, 'varchar', 'varchar(255)', b'0', b'0', b'1', NULL, 10, '用户名', 'username', 'String', NULL, NULL, 0, b'0', b'0', b'0', NULL, NULL, 0, NULL, NULL, NULL, '2024-07-05 15:48:36', 1808020007993479168, '2024-07-05 15:49:49', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_column_rule +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_column_rule`; +CREATE TABLE `zz_online_column_rule` ( + `column_id` bigint NOT NULL COMMENT '字段Id', + `rule_id` bigint NOT NULL COMMENT '规则Id', + `prop_data_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '规则属性数据', + PRIMARY KEY (`column_id`,`rule_id`) USING BTREE, + KEY `idx_rule_id` (`rule_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字段和字段规则关联中间表'; + +-- ---------------------------- +-- Table structure for zz_online_datasource +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_datasource`; +CREATE TABLE `zz_online_datasource` ( + `datasource_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `datasource_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据源名称', + `variable_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据源变量名', + `dblink_id` bigint NOT NULL COMMENT '数据库链接Id', + `master_table_id` bigint NOT NULL COMMENT '主表Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`datasource_id`), + UNIQUE KEY `uk_app_code_variable_name` (`app_code`,`variable_name`) USING BTREE, + KEY `idx_master_table_id` (`master_table_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据源表'; + +-- ---------------------------- +-- Records of zz_online_datasource +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_datasource` VALUES (1809132255981867008, NULL, '请假申请', 'dsLeave', 1809055300360081408, 1809132251556876288, '2024-07-05 15:48:37', 1808020007993479168, '2024-07-05 15:48:37', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_datasource_relation +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_datasource_relation`; +CREATE TABLE `zz_online_datasource_relation` ( + `relation_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `relation_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '关联名称', + `variable_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '变量名', + `datasource_id` bigint NOT NULL COMMENT '主数据源Id', + `relation_type` int NOT NULL COMMENT '关联类型', + `master_column_id` bigint NOT NULL COMMENT '主表关联字段Id', + `slave_table_id` bigint NOT NULL COMMENT '从表Id', + `slave_column_id` bigint NOT NULL COMMENT '从表关联字段Id', + `cascade_delete` bit(1) NOT NULL COMMENT '删除主表的时候是否级联删除一对一和一对多的从表数据,多对多只是删除关联,不受到这个标记的影响。', + `left_join` bit(1) NOT NULL COMMENT '是否左连接', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`relation_id`) USING BTREE, + UNIQUE KEY `uk_datasource_id_variable_name` (`datasource_id`,`variable_name`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据源关联表'; + +-- ---------------------------- +-- Table structure for zz_online_datasource_table +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_datasource_table`; +CREATE TABLE `zz_online_datasource_table` ( + `id` bigint NOT NULL COMMENT '主键Id', + `datasource_id` bigint NOT NULL COMMENT '数据源Id', + `relation_id` bigint DEFAULT NULL COMMENT '数据源关联Id', + `table_id` bigint NOT NULL COMMENT '数据表Id', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_relation_id` (`relation_id`) USING BTREE, + KEY `idx_datasource_id` (`datasource_id`) USING BTREE, + KEY `idx_table_id` (`table_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据源和数据表关联的中间表'; + +-- ---------------------------- +-- Records of zz_online_datasource_table +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_datasource_table` VALUES (1809132256292245504, 1809132255981867008, NULL, 1809132251556876288); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_dblink +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_dblink`; +CREATE TABLE `zz_online_dblink` ( + `dblink_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `dblink_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '链接中文名称', + `dblink_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接描述', + `dblink_type` int NOT NULL COMMENT '数据源类型', + `configuration` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '配置信息', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`dblink_id`), + KEY `idx_dblink_type` (`dblink_type`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据库链接表'; + +-- ---------------------------- +-- Records of zz_online_dblink +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_dblink` VALUES (1809055300360081408, NULL, 'mysql-test', NULL, 0, '{\"sid\":true,\"initialPoolSize\":5,\"minPoolSize\":5,\"maxPoolSize\":50,\"host\":\"localhost\",\"port\":3306,\"database\":\"zzdemo-online-open\",\"username\":\"root\",\"password\":\"123456\"}', '2024-07-05 10:42:49', 1809038124504846336, '2024-07-05 10:42:49', 1809038124504846336); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_dict +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_dict`; +CREATE TABLE `zz_online_dict` ( + `dict_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `dict_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字典名称', + `dict_type` int NOT NULL COMMENT '字典类型', + `dblink_id` bigint DEFAULT NULL COMMENT '数据库链接Id', + `table_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典表名称', + `dict_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '全局字典编码', + `key_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典表键字段名称', + `parent_key_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典表父键字段名称', + `value_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字典值字段名称', + `deleted_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '逻辑删除字段', + `user_filter_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户过滤滤字段名称', + `dept_filter_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '部门过滤滤字段名称', + `tenant_filter_column_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '租户过滤字段名称', + `tree_flag` bit(1) NOT NULL COMMENT '是否树形标记', + `dict_list_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '获取字典列表数据的url', + `dict_ids_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '根据主键id批量获取字典数据的url', + `dict_data_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '字典的JSON数据', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`dict_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字典表'; + +-- ---------------------------- +-- Table structure for zz_online_form +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_form`; +CREATE TABLE `zz_online_form` ( + `form_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `page_id` bigint NOT NULL COMMENT '页面id', + `form_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '表单编码', + `form_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '表单名称', + `form_kind` int NOT NULL COMMENT '表单类别', + `form_type` int NOT NULL COMMENT '表单类型', + `master_table_id` bigint NOT NULL COMMENT '表单主表id', + `widget_json` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '表单组件JSON', + `params_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '表单参数JSON', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`form_id`) USING BTREE, + UNIQUE KEY `uk_page_id_form_code` (`page_id`,`form_code`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单表单表'; + +-- ---------------------------- +-- Records of zz_online_form +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_form` VALUES (1809132635633487872, NULL, NULL, 1809132177523216384, 'formFlowLeave', '请假申请', 5, 10, 1809132251556876288, '{\"pc\":{\"gutter\":20,\"labelWidth\":100,\"labelPosition\":\"right\",\"operationList\":[],\"customFieldList\":[],\"widgetList\":[{\"widgetType\":3,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132253377204224\",\"dataType\":0},\"showName\":\"请假类型\",\"variableName\":\"leaveType\",\"props\":{\"span\":24,\"placeholder\":\"\",\"step\":1,\"controls\":true,\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{}},{\"widgetType\":1,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132252852916224\",\"dataType\":0},\"showName\":\"请假原因\",\"variableName\":\"leaveReason\",\"props\":{\"span\":24,\"type\":\"text\",\"placeholder\":\"\",\"show-password\":false,\"show-word-limit\":false,\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{}},{\"widgetType\":20,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132253733720064\",\"dataType\":0},\"showName\":\"开始时间\",\"variableName\":\"leaveBeginTime\",\"props\":{\"span\":12,\"placeholder\":\"\",\"type\":\"date\",\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{},\"supportOperation\":false},{\"widgetType\":20,\"bindData\":{\"defaultValue\":{},\"tableId\":\"1809132251556876288\",\"columnId\":\"1809132254102818816\",\"dataType\":0},\"showName\":\"结束时间\",\"variableName\":\"leaveEndTime\",\"props\":{\"span\":12,\"placeholder\":\"\",\"type\":\"date\",\"required\":true,\"disabled\":false,\"dictInfo\":{\"paramList\":[]},\"actions\":{}},\"eventList\":[],\"childWidgetList\":[],\"style\":{},\"supportOperation\":false}],\"formEventList\":[],\"maskFieldList\":[],\"width\":800,\"fullscreen\":true}}', NULL, '2024-07-05 15:50:07', 1808020007993479168, '2024-07-05 16:34:21', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_form_datasource +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_form_datasource`; +CREATE TABLE `zz_online_form_datasource` ( + `id` bigint NOT NULL COMMENT '主键Id', + `form_id` bigint NOT NULL COMMENT '表单Id', + `datasource_id` bigint NOT NULL COMMENT '数据源Id', + PRIMARY KEY (`id`), + KEY `idx_form_id` (`form_id`) USING BTREE, + KEY `idx_datasource_id` (`datasource_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单表单和数据源关联中间表'; + +-- ---------------------------- +-- Records of zz_online_form_datasource +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_form_datasource` VALUES (1809143766578106368, 1809132635633487872, 1809132255981867008); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_page +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_page`; +CREATE TABLE `zz_online_page` ( + `page_id` bigint NOT NULL COMMENT '主键Id', + `tenant_id` bigint DEFAULT NULL COMMENT '租户id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `page_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '页面编码', + `page_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '页面名称', + `page_type` int NOT NULL COMMENT '页面类型', + `status` int NOT NULL COMMENT '页面编辑状态', + `published` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否发布', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`page_id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE, + KEY `idx_page_code` (`page_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单页面表'; + +-- ---------------------------- +-- Records of zz_online_page +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_page` VALUES (1809132177523216384, NULL, NULL, 'flowLeave', '请假申请', 10, 2, b'1', '2024-07-05 15:48:18', 1808020007993479168, '2024-07-05 16:34:27', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_page_datasource +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_page_datasource`; +CREATE TABLE `zz_online_page_datasource` ( + `id` bigint NOT NULL COMMENT '主键Id', + `page_id` bigint NOT NULL COMMENT '页面主键Id', + `datasource_id` bigint NOT NULL COMMENT '数据源主键Id', + PRIMARY KEY (`id`), + KEY `idx_page_id` (`page_id`) USING BTREE, + KEY `idx_datasource_id` (`datasource_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单页面和数据源关联中间表'; + +-- ---------------------------- +-- Records of zz_online_page_datasource +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_page_datasource` VALUES (1809132256564875264, 1809132177523216384, 1809132255981867008); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_rule +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_rule`; +CREATE TABLE `zz_online_rule` ( + `rule_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `rule_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '规则名称', + `rule_type` int NOT NULL COMMENT '规则类型', + `builtin` bit(1) NOT NULL COMMENT '内置规则标记', + `pattern` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '自定义规则的正则表达式', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + `deleted_flag` int NOT NULL COMMENT '逻辑删除标记', + PRIMARY KEY (`rule_id`) USING BTREE, + KEY `idx_app_code` (`app_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单字段规则表'; + +-- ---------------------------- +-- Records of zz_online_rule +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_rule` VALUES (1, NULL, '只允许整数', 1, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (2, NULL, '只允许数字', 2, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (3, NULL, '只允许英文字符', 3, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (4, NULL, '范围验证', 4, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (5, NULL, '邮箱格式验证', 5, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +INSERT INTO `zz_online_rule` VALUES (6, NULL, '手机格式验证', 6, b'1', NULL, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_table +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_table`; +CREATE TABLE `zz_online_table` ( + `table_id` bigint NOT NULL COMMENT '主键Id', + `app_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用编码', + `table_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '表名称', + `model_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '实体名称', + `dblink_id` bigint NOT NULL COMMENT '数据库链接Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `create_user_id` bigint NOT NULL COMMENT '创建者', + `update_time` datetime NOT NULL COMMENT '更新时间', + `update_user_id` bigint NOT NULL COMMENT '更新者', + PRIMARY KEY (`table_id`), + KEY `idx_dblink_id` (`dblink_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单数据表'; + +-- ---------------------------- +-- Records of zz_online_table +-- ---------------------------- +BEGIN; +INSERT INTO `zz_online_table` VALUES (1809132251556876288, NULL, 'zz_test_flow_leave', 'ZzTestFlowLeave', 1809055300360081408, '2024-07-05 15:48:35', 1808020007993479168, '2024-07-05 15:48:35', 1808020007993479168); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_online_virtual_column +-- ---------------------------- +DROP TABLE IF EXISTS `zz_online_virtual_column`; +CREATE TABLE `zz_online_virtual_column` ( + `virtual_column_id` bigint NOT NULL COMMENT '主键Id', + `table_id` bigint NOT NULL COMMENT '所在表Id', + `object_field_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字段名称', + `object_field_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '属性类型', + `column_prompt` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '字段提示名', + `virtual_type` int NOT NULL COMMENT '虚拟字段类型(0: 聚合)', + `datasource_id` bigint NOT NULL COMMENT '关联数据源Id', + `relation_id` bigint DEFAULT NULL COMMENT '关联Id', + `aggregation_table_id` bigint DEFAULT NULL COMMENT '聚合字段所在关联表Id', + `aggregation_column_id` bigint DEFAULT NULL COMMENT '关联表聚合字段Id', + `aggregation_type` int DEFAULT NULL COMMENT '聚合类型(0: sum 1: count 2: avg 3: min 4: max)', + `where_clause_json` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '存储过滤条件的json', + PRIMARY KEY (`virtual_column_id`) USING BTREE, + KEY `idx_database_id` (`datasource_id`) USING BTREE, + KEY `idx_relation_id` (`relation_id`) USING BTREE, + KEY `idx_table_id` (`table_id`) USING BTREE, + KEY `idx_aggregation_column_id` (`aggregation_column_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='在线表单虚拟字段表'; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm`; +CREATE TABLE `zz_sys_data_perm` ( + `data_perm_id` bigint NOT NULL COMMENT '主键', + `data_perm_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '显示名称', + `rule_type` tinyint NOT NULL COMMENT '数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`data_perm_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限表'; + +-- ---------------------------- +-- Records of zz_sys_data_perm +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_data_perm` VALUES (1809037881759502336, '查看全部', 0, 1808020007993479168, '2024-07-05 09:33:36', 1808020007993479168, '2024-07-05 09:33:36'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm_dept +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm_dept`; +CREATE TABLE `zz_sys_data_perm_dept` ( + `data_perm_id` bigint NOT NULL COMMENT '数据权限Id', + `dept_id` bigint NOT NULL COMMENT '部门Id', + PRIMARY KEY (`data_perm_id`,`dept_id`), + KEY `idx_dept_id` (`dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限和部门关联表'; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm_menu +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm_menu`; +CREATE TABLE `zz_sys_data_perm_menu` ( + `data_perm_id` bigint NOT NULL COMMENT '数据权限Id', + `menu_id` bigint NOT NULL COMMENT '菜单Id', + PRIMARY KEY (`data_perm_id`,`menu_id`), + KEY `idx_menu_id` (`menu_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限和菜单关联表'; + +-- ---------------------------- +-- Table structure for zz_sys_data_perm_user +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_data_perm_user`; +CREATE TABLE `zz_sys_data_perm_user` ( + `data_perm_id` bigint NOT NULL COMMENT '数据权限Id', + `user_id` bigint NOT NULL COMMENT '用户Id', + PRIMARY KEY (`data_perm_id`,`user_id`), + KEY `idx_user_id` (`user_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据权限和用户关联表'; + +-- ---------------------------- +-- Records of zz_sys_data_perm_user +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_data_perm_user` VALUES (1809037881759502336, 1809038124504846336); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_dept +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_dept`; +CREATE TABLE `zz_sys_dept` ( + `dept_id` bigint NOT NULL COMMENT '部门Id', + `parent_id` bigint DEFAULT NULL COMMENT '父部门Id', + `dept_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门名称', + `show_order` int NOT NULL COMMENT '兄弟部分之间的显示顺序,数字越小越靠前', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`dept_id`) USING BTREE, + KEY `idx_parent_id` (`parent_id`) USING BTREE, + KEY `idx_show_order` (`show_order`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='部门管理表'; + +-- ---------------------------- +-- Records of zz_sys_dept +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_dept` VALUES (1808020008341606402, NULL, '公司总部', 1, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_dept_post +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_dept_post`; +CREATE TABLE `zz_sys_dept_post` ( + `dept_post_id` bigint NOT NULL COMMENT '主键Id', + `dept_id` bigint NOT NULL COMMENT '部门Id', + `post_id` bigint NOT NULL COMMENT '岗位Id', + `post_show_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门岗位显示名称', + PRIMARY KEY (`dept_post_id`) USING BTREE, + KEY `idx_post_id` (`post_id`) USING BTREE, + KEY `idx_dept_id` (`dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_sys_dept_post +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_dept_post` VALUES (1809038003536924672, 1808020008341606402, 1809037927934595072, '领导岗位'); +INSERT INTO `zz_sys_dept_post` VALUES (1809038003968937984, 1808020008341606402, 1809037967663042560, '普通员工'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_dept_relation +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_dept_relation`; +CREATE TABLE `zz_sys_dept_relation` ( + `parent_dept_id` bigint NOT NULL COMMENT '父部门Id', + `dept_id` bigint NOT NULL COMMENT '部门Id', + PRIMARY KEY (`parent_dept_id`,`dept_id`), + KEY `idx_dept_id` (`dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='部门关联关系表'; + +-- ---------------------------- +-- Records of zz_sys_dept_relation +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_dept_relation` VALUES (1808020008341606402, 1808020008341606402); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_menu`; +CREATE TABLE `zz_sys_menu` ( + `menu_id` bigint NOT NULL COMMENT '主键Id', + `parent_id` bigint DEFAULT NULL COMMENT '父菜单Id,目录菜单的父菜单为null', + `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '菜单显示名称', + `menu_type` int NOT NULL COMMENT '(0: 目录 1: 菜单 2: 按钮 3: UI片段)', + `form_router_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '前端表单路由名称,仅用于menu_type为1的菜单类型', + `online_form_id` bigint DEFAULT NULL COMMENT '在线表单主键Id', + `online_menu_perm_type` int DEFAULT NULL COMMENT '在线表单菜单的权限控制类型', + `report_page_id` bigint DEFAULT NULL COMMENT '统计页面主键Id', + `online_flow_entry_id` bigint DEFAULT NULL COMMENT '仅用于在线表单的流程Id', + `show_order` int NOT NULL COMMENT '菜单显示顺序 (值越小,排序越靠前)', + `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '菜单图标', + `extra_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '附加信息', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`menu_id`) USING BTREE, + KEY `idx_show_order` (`show_order`) USING BTREE, + KEY `idx_parent_id` (`parent_id`) USING BTREE, + KEY `idx_menu_type` (`menu_type`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='菜单和操作权限管理表'; + +-- ---------------------------- +-- Records of zz_sys_menu +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_menu` VALUES (1392786476428693504, NULL, '在线表单', 0, NULL, NULL, NULL, NULL, NULL, 2, 'el-icon-c-scale-to-original', NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1392786549942259712, 1392786476428693504, '字典管理', 1, 'formOnlineDict', NULL, NULL, NULL, NULL, 2, NULL, '{\"permCodeList\":[\"onlineDict.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1392786950682841088, 1392786476428693504, '表单管理', 1, 'formOnlinePage', NULL, NULL, NULL, NULL, 3, NULL, '{\"permCodeList\":[\"onlinePage.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418057714138877952, NULL, '流程管理', 0, NULL, NULL, NULL, NULL, NULL, 3, 'el-icon-s-operation', NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418057835631087616, 1418057714138877952, '流程分类', 1, 'formFlowCategory', NULL, NULL, NULL, NULL, 1, NULL, '{\"permCodeList\":[\"flowCategory.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418058289182150656, 1418057714138877952, '流程设计', 1, 'formFlowEntry', NULL, NULL, NULL, NULL, 2, NULL, '{\"permCodeList\":[\"flowEntry.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418058744037642240, 1418057714138877952, '流程实例', 1, 'formAllInstance', NULL, NULL, NULL, NULL, 3, NULL, '{\"permCodeList\":[\"flowOperation.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418059005175009280, NULL, '任务管理', 0, NULL, NULL, NULL, NULL, NULL, 4, 'el-icon-tickets', NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418059167532322816, 1418059005175009280, '待办任务', 1, 'formMyTask', NULL, NULL, NULL, NULL, 1, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1418059283920064512, 1418059005175009280, '历史任务', 1, 'formMyHistoryTask', NULL, NULL, NULL, NULL, 3, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1423161217970606080, 1418059005175009280, '已办任务', 1, 'formMyApprovedTask', NULL, NULL, NULL, NULL, 2, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1634009076981567488, 1392786476428693504, '数据库链接', 1, 'formOnlineDblink', NULL, NULL, NULL, NULL, 1, NULL, '{\"permCodeList\":[\"onlineDblink.all\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020011080486913, NULL, '系统管理', 0, NULL, NULL, NULL, NULL, NULL, 1, 'el-icon-setting', '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317376, 1808020011080486913, '用户管理', 1, 'formSysUser', NULL, NULL, NULL, NULL, 100, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317377, 1808020011080486913, '部门管理', 1, 'formSysDept', NULL, NULL, NULL, NULL, 105, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317378, 1808020011080486913, '角色管理', 1, 'formSysRole', NULL, NULL, NULL, NULL, 110, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317379, 1808020011080486913, '数据权限管理', 1, 'formSysDataPerm', NULL, NULL, NULL, NULL, 115, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317380, 1808020011080486913, '岗位管理', 1, 'formSysPost', NULL, NULL, NULL, NULL, 106, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317381, 1808020011080486913, '菜单管理', 1, 'formSysMenu', NULL, NULL, NULL, NULL, 120, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317384, 1808020011080486913, '字典管理', 1, 'formSysDict', NULL, NULL, NULL, NULL, 135, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317385, 1808020011080486913, '操作日志', 1, 'formSysOperationLog', NULL, NULL, NULL, NULL, 140, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020012825317386, 1808020011080486913, '在线用户', 1, 'formSysLoginUser', NULL, NULL, NULL, NULL, 145, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148866, 1808020012825317376, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser\",\"permCodeList\":[\"sysUser.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148867, 1808020012825317376, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:add\",\"permCodeList\":[\"sysUser.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148868, 1808020012825317376, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:update\",\"permCodeList\":[\"sysUser.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148869, 1808020012825317376, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:delete\",\"permCodeList\":[\"sysUser.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148870, 1808020012825317376, '重置密码', 3, NULL, NULL, NULL, NULL, NULL, 5, NULL, '{\"menuCode\":\"formSysUser:fragmentSysUser:resetPassword\",\"permCodeList\":[\"sysUser.resetPassword\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148872, 1808020012825317377, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept\",\"permCodeList\":[\"sysDept.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148873, 1808020012825317377, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, '', '{\"bindType\":0,\"menuCode\":\"formSysDept:fragmentSysDept:add\",\"permCodeList\":[\"sysDept.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-05 09:51:07'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148874, 1808020012825317377, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:update\",\"permCodeList\":[\"sysDept.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148875, 1808020012825317377, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:delete\",\"permCodeList\":[\"sysDept.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148876, 1808020012825317377, '设置岗位', 3, NULL, NULL, NULL, NULL, NULL, 5, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:editPost\",\"permCodeList\":[\"sysDept.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148877, 1808020012825317377, '查看岗位', 3, NULL, NULL, NULL, NULL, NULL, 6, NULL, '{\"menuCode\":\"formSysDept:fragmentSysDept:viewPost\",\"permCodeList\":[\"sysDept.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148879, 1808020012825317378, '角色管理', 2, NULL, NULL, NULL, NULL, NULL, 1, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148880, 1808020012825317378, '用户授权', 2, NULL, NULL, NULL, NULL, NULL, 2, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148881, 1808020075098148879, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole\",\"permCodeList\":[\"sysRole.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148882, 1808020075098148879, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole:add\",\"permCodeList\":[\"sysRole.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148883, 1808020075098148879, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole:update\",\"permCodeList\":[\"sysRole.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148884, 1808020075098148879, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRole:delete\",\"permCodeList\":[\"sysRole.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148885, 1808020075098148880, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRoleUser\",\"permCodeList\":[\"sysRole.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148886, 1808020075098148880, '授权用户', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRoleUser:addUserRole\",\"permCodeList\":[\"sysRole.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148887, 1808020075098148880, '移除用户', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysRole:fragmentSysRoleUser:deleteUserRole\",\"permCodeList\":[\"sysRole.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148889, 1808020012825317379, '数据权限管理', 2, NULL, NULL, NULL, NULL, NULL, 1, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148890, 1808020012825317379, '用户授权', 2, NULL, NULL, NULL, NULL, NULL, 2, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148891, 1808020075098148889, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm\",\"permCodeList\":[\"sysDataPerm.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148892, 1808020075098148889, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm:add\",\"permCodeList\":[\"sysDataPerm.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148893, 1808020075098148889, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm:update\",\"permCodeList\":[\"sysDataPerm.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148894, 1808020075098148889, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPerm:delete\",\"permCodeList\":[\"sysDataPerm.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148895, 1808020075098148890, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPermUser\",\"permCodeList\":[\"sysDataPerm.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148896, 1808020075098148890, '授权用户', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPermUser:addDataPermUser\",\"permCodeList\":[\"sysDataPerm.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148897, 1808020075098148890, '移除用户', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDataPerm:fragmentSysDataPermUser:deleteDataPermUser\",\"permCodeList\":[\"sysDataPerm.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148899, 1808020012825317380, '岗位管理', 2, NULL, NULL, NULL, NULL, NULL, 1, NULL, '', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148900, 1808020075098148899, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost\",\"permCodeList\":[\"sysPost.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148901, 1808020075098148899, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost:add\",\"permCodeList\":[\"sysPost.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148902, 1808020075098148899, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost:update\",\"permCodeList\":[\"sysPost.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148903, 1808020075098148899, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysPost:fragmentSysPost:delete\",\"permCodeList\":[\"sysPost.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148905, 1808020012825317381, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu\",\"permCodeList\":[\"sysMenu.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148906, 1808020012825317381, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu:add\",\"permCodeList\":[\"sysMenu.add\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148907, 1808020012825317381, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu:update\",\"permCodeList\":[\"sysMenu.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075098148908, 1808020012825317381, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysMenu:fragmentSysMenu:delete\",\"permCodeList\":[\"sysMenu.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343171, 1808020012825317384, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict\",\"permCodeList\":[\"globalDict.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343172, 1808020012825317384, '新增', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:add\",\"permCodeList\":[\"globalDict.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343173, 1808020012825317384, '编辑', 3, NULL, NULL, NULL, NULL, NULL, 3, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:update\",\"permCodeList\":[\"globalDict.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343174, 1808020012825317384, '删除', 3, NULL, NULL, NULL, NULL, NULL, 4, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:delete\",\"permCodeList\":[\"globalDict.update\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343175, 1808020012825317384, '同步缓存', 3, NULL, NULL, NULL, NULL, NULL, 5, NULL, '{\"menuCode\":\"formSysDict:fragmentSysDict:reloadCache\",\"permCodeList\":[\"globalDict.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343177, 1808020012825317385, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysOperationLog:fragmentSysOperationLog\",\"permCodeList\":[\"sysOperationLog.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343179, 1808020012825317386, '显示', 3, NULL, NULL, NULL, NULL, NULL, 1, NULL, '{\"menuCode\":\"formSysLoginUser:fragmentLoginUser\",\"permCodeList\":[\"loginUser.view\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +INSERT INTO `zz_sys_menu` VALUES (1808020075102343180, 1808020012825317386, '强制下线', 3, NULL, NULL, NULL, NULL, NULL, 2, NULL, '{\"menuCode\":\"formSysLoginUser:fragmentLoginUser:delete\",\"permCodeList\":[\"loginUser.delete\"]}', 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_operation_log +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_operation_log`; +CREATE TABLE `zz_sys_operation_log` ( + `log_id` bigint NOT NULL COMMENT '主键Id', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '日志描述', + `operation_type` int DEFAULT NULL COMMENT '操作类型', + `service_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '接口所在服务名称', + `api_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '调用的controller全类名', + `api_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '调用的controller中的方法', + `session_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户会话sessionId', + `trace_id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '每次请求的Id', + `elapse` int DEFAULT NULL COMMENT '调用时长', + `request_method` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'HTTP 请求方法,如GET', + `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'HTTP 请求地址', + `request_arguments` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT 'controller接口参数', + `response_result` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'controller应答结果', + `request_ip` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求IP', + `success` bit(1) DEFAULT NULL COMMENT '应答状态', + `error_msg` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '错误信息', + `tenant_id` bigint DEFAULT NULL COMMENT '租户Id', + `operator_id` bigint DEFAULT NULL COMMENT '操作员Id', + `operator_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作员名称', + `operation_time` datetime DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`log_id`), + KEY `idx_trace_id_idx` (`trace_id`), + KEY `idx_operation_type_idx` (`operation_type`), + KEY `idx_operation_time_idx` (`operation_time`) USING BTREE, + KEY `idx_success` (`success`) USING BTREE, + KEY `idx_elapse` (`elapse`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='系统操作日志表'; + +-- ---------------------------- +-- Records of zz_sys_operation_log +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_operation_log` VALUES (1809037495178891264, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'c5eafaee0e294b3b8fe1ddc47a73aa6f', 526, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"U7kblCgd8NWaoUrEH%2B0j0ocRESztOUkH4L1eMANf40rAVWfgTmw8w1D2QeH2b99bxJQRCoELhiJDo3NbdN8sodZf%2BWa%2BRoH8URHmG1qziSMw4C%2Fc40gR1x4vclxMrq9jN1d3yP2gVljlaxVmMQcVsLqGsgcxfvyucwYzClifRUY%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 09:32:04'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037516607590400, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'f1104bc680014a999321a6ca3c240485', 136, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"MYjsPjZgslAadC1%2FhwPRNyG5yvtl%2BRVWJGOj0MfPNNJyTMMBgPrymEsoMsR%2FnSog7TdIborw%2BYgO9o31KFowqf3I3Gw6oI0qXkDbJKBqeDqkKKoOa95J9ITm7TKHKYcKu15xhmQvmU1OIMs59A2w39Cx1Z58I7gtbtHHL34iVJg%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 09:32:09'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037535469375488, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '58de29f3ec22457d8f4f980a350cf623', 579, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"i%2BcOZFUuWVmCCh%2B1ZhtpXTD8RNG1S4GMABC0dZssCPYckczkR%2FeRSuiYCMlDLaUa1oN%2BPeZRvj3zPKmDcuDyi0Jewxq7kTFyFAy%2Fbrep5MD3i2X%2BtV9B%2FT3CMMdbdOMa1OVP1AUO%2FBbmGdu0iK3UpvL608mJx1vqbpLRynYBazc%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:32:13'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037772132978688, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysRoleController', 'com.orangeforms.webadmin.upms.controller.SysRoleController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', 'cd5eb86b69094458881aa6dcb04aa766', 5496, 'POST', '/admin/upms/sysRole/add', '{\"menuIdListString\":\"1392786476428693504,1392786549942259712,1392786950682841088,1634009076981567488,1418057714138877952,1418057835631087616,1418058289182150656,1418058744037642240,1418059005175009280,1418059167532322816,1418059283920064512,1423161217970606080,1808020011080486913,1808020012825317376,1808020075098148866,1808020075098148867,1808020075098148868,1808020075098148869,1808020075098148870,1808020012825317377,1808020075098148872,1808020075098148873,1808020075098148874,1808020075098148875,1808020075098148876,1808020075098148877,1808020012825317378,1808020075098148879,1808020075098148881,1808020075098148882,1808020075098148883,1808020075098148884,1808020075098148880,1808020075098148885,1808020075098148886,1808020075098148887,1808020012825317379,1808020075098148889,1808020075098148891,1808020075098148892,1808020075098148893,1808020075098148894,1808020075098148890,1808020075098148895,1808020075098148896,1808020075098148897,1808020012825317380,1808020075098148899,1808020075098148900,1808020075098148901,1808020075098148902,1808020075098148903,1808020012825317381,1808020075098148905,1808020075098148906,1808020075098148907,1808020075098148908,1808020012825317384,1808020075102343171,1808020075102343172,1808020075102343173,1808020075102343174,1808020075102343175,1808020012825317385,1808020075102343177,1808020012825317386,1808020075102343179,1808020075102343180\",\"sysRoleDto\":{\"roleName\":\"查看全部\"}}', '{\"data\":1809037772728569856,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:10'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037881738530816, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysDataPermController', 'com.orangeforms.webadmin.upms.controller.SysDataPermController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '85746a39a3a34191884eb30453ecc237', 220, 'POST', '/admin/upms/sysDataPerm/add', '{\"sysDataPermDto\":{\"dataPermName\":\"查看全部\",\"ruleType\":0},\"menuIdListString\":\"\"}', '{\"data\":1809037881759502336,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:36'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037927917817856, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysPostController', 'com.orangeforms.webadmin.upms.controller.SysPostController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', 'fa4bf0f0b80748249bdfb4c78bb93b8d', 190, 'POST', '/admin/upms/sysPost/add', '{\"sysPostDto\":{\"leaderPost\":true,\"postLevel\":1,\"postName\":\"领导岗位\"}}', '{\"data\":1809037927934595072,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:47'); +INSERT INTO `zz_sys_operation_log` VALUES (1809037967658848256, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysPostController', 'com.orangeforms.webadmin.upms.controller.SysPostController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '04da87ca21294e0296af2e162999e396', 228, 'POST', '/admin/upms/sysPost/add', '{\"sysPostDto\":{\"postLevel\":10,\"postName\":\"普通员工\"}}', '{\"data\":1809037967663042560,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:33:56'); +INSERT INTO `zz_sys_operation_log` VALUES (1809038123905060864, '', 10, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysUserController', 'com.orangeforms.webadmin.upms.controller.SysUserController.add', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '99bb05eadf944b54a194db3152773b13', 635, 'POST', '/admin/upms/sysUser/add', '{\"sysUserDto\":{\"deptId\":1808020008341606402,\"loginName\":\"userA\",\"password\":\"123456\",\"showName\":\"员工A\",\"userStatus\":0,\"userType\":2},\"dataPermIdListString\":\"1809037881759502336\",\"deptPostIdListString\":\"1809038003968937984\",\"roleIdListString\":\"1809037772728569856\"}', '{\"data\":1809038124504846336,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:34:34'); +INSERT INTO `zz_sys_operation_log` VALUES (1809042287854882816, '', 15, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysMenuController', 'com.orangeforms.webadmin.upms.controller.SysMenuController.update', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '68dd67ca9821460caee6986c5c3c3354', 420, 'POST', '/admin/upms/sysMenu/update', '{\"sysMenuDto\":{\"extraData\":\"{\\\"bindType\\\":0,\\\"menuCode\\\":\\\"formSysDept:fragmentSysDept:add\\\",\\\"permCodeList\\\":[\\\"sysDept.add\\\"]}\",\"icon\":\"\",\"menuId\":1808020075098148873,\"menuName\":\"新增\",\"menuType\":3,\"parentId\":1808020012825317377,\"showOrder\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 09:51:06'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050375580291072, '', 5, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogout', 'Authorization:login:token-session:5fb5b15d-2b4c-4063-ae55-5b0ec195fa39', '8611984bfad74113bcc5f5a2d30f0557', 36, 'POST', '/admin/upms/login/doLogout', '{}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 10:23:15'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050381297127424, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'bb940a20dbac4f11b7d448ebe11668c4', 466, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"jiRS2mxriWjx778WM%2FJql65bpRfu7BaqVkPrDySclvJ7%2B%2B0KSuAIZ557bEFocQnCWbfLJwRFokTUDastSpEeiFAsd1kwv6oZyQimj4KCyDtin6P6gPsn2GRQrFKACkOKBXY70FeGgQvaVwWBEGo6EzdfJw9adJOGf2WIigrIajk%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 10:23:16'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050432673157120, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, 'ffe77e34ec35454da6e71b0cef7f2ea8', 143, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"Kdkf8xz%2Fay2lKRidpUGWBJM7%2BlvxTVpjdSNLCuL1yx6LbVvTPo7PD5zFBLKMPWeSrtostyAFybz6lAAHpdCnQWjmbBbpMExTmY74O12EQySXOQBwrmH3yltq9MXJI5qRJ24imMxYyTvcX2yDMbEfDF3zcC404GvTgX0gexCmTjs%3D\",\"loginName\":\"userA\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 10:23:28'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050456257728512, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'b43789173f9244aa803866db7bafca73', 460, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"gYCq1nWZHSsvg35HCgnRzw23kN3PRTZJY%2Bt2bcZWliYf11o14OHEDhsH12nCC4LYn00UEDoYWbbMdiwNzQFmcgmbJq4%2Fu6uxURokHpI%2BEexZnL5IzWBb2P53hGBwUkOO36jRfbTm%2B0qRtIbpATs74jpc1L%2FFbT18%2Fj%2FN9C3bpq4%3D\",\"loginName\":\"userA\"}', NULL, '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:23:34'); +INSERT INTO `zz_sys_operation_log` VALUES (1809050496074256384, '', 15, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.SysUserController', 'com.orangeforms.webadmin.upms.controller.SysUserController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '8926e20ea352417aab2234d2de2c1fea', 903, 'POST', '/admin/upms/sysUser/update', '{\"sysUserDto\":{\"deptId\":1808020008341606402,\"loginName\":\"userA\",\"showName\":\"员工A\",\"userId\":1809038124504846336,\"userStatus\":0,\"userType\":2},\"dataPermIdListString\":\"1809037881759502336\",\"deptPostIdListString\":\"1809038003968937984\",\"roleIdListString\":\"1809037772728569856\"}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:23:43'); +INSERT INTO `zz_sys_operation_log` VALUES (1809051198259466240, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowCategoryController', 'com.orangeforms.common.flow.controller.FlowCategoryController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'cd9d22e5fd194e7dac6c90531cab52dd', 249, 'POST', '/admin/flow/flowCategory/add', '{\"flowCategoryDto\":{\"code\":\"TEST\",\"name\":\"测试分类\",\"showOrder\":1}}', '{\"data\":1809051198460792832,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:26:31'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052045043306496, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '66225485743a44df81882b0b70885c69', 478, 'POST', '/admin/flow/flowEntry/add', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\"}}', '{\"data\":1809052045395628032,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:29:53'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052080904605696, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryVariableController', 'com.orangeforms.common.flow.controller.FlowEntryVariableController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'c7804ebbe24d492b82c23cb5f3b25879', 225, 'POST', '/admin/flow/flowEntryVariable/add', '{\"flowEntryVariableDto\":{\"builtin\":false,\"entryId\":1809052045395628032,\"showName\":\"AAA\",\"variableName\":\"aaa\",\"variableType\":1}}', '{\"data\":1809052080921382912,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:30:01'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052112206696448, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '9c28172a4ac74b448439334f0ea44d34', 306, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11}],\\\"notifyTypes\\\":[]}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:30:09'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052122159779840, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'c9af56a061bb4a87a964cb617f4f9c15', 201, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11,\\\"_X_ROW_KEY\\\":\\\"row_33\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:30:11'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052746851028992, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '6609a8762f6c49af9f570b746a30b8ac', 297, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11,\\\"_X_ROW_KEY\\\":\\\"row_33\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\",\"status\":0}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:32:40'); +INSERT INTO `zz_sys_operation_log` VALUES (1809052753826156544, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'b1c15eb123b14ae68876d1e026b8f498', 267, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":1,\"categoryId\":1809051198460792832,\"defaultRouterName\":\"AAA\",\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"AA\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809052045395628032,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_28\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_29\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_30\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_31\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_32\\\"},{\\\"name\\\":\\\"AAA\\\",\\\"id\\\":11,\\\"_X_ROW_KEY\\\":\\\"row_33\\\"}],\\\"notifyTypes\\\":[],\\\"cascadeDeleteBusinessData\\\":false,\\\"supportRevive\\\":false}\",\"processDefinitionKey\":\"AAA\",\"processDefinitionName\":\"AAA\",\"status\":0}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:32:42'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055300347498496, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineDblinkController', 'com.orangeforms.common.online.controller.OnlineDblinkController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '4552709db33b4ac38520c97cbfbd84fb', 180, 'POST', '/admin/online/onlineDblink/add', '{\"onlineDblinkDto\":{\"configuration\":\"{\\\"sid\\\":true,\\\"initialPoolSize\\\":5,\\\"minPoolSize\\\":5,\\\"maxPoolSize\\\":50,\\\"host\\\":\\\"121.37.102.103\\\",\\\"port\\\":3306,\\\"database\\\":\\\"zzdemo-online-open\\\",\\\"username\\\":\\\"root\\\",\\\"password\\\":\\\"TianLiujielei231\\\"}\",\"dblinkName\":\"mysql-test\",\"dblinkType\":0}}', '{\"data\":1809055300360081408,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:42:49'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055451015286784, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'f6a28f34b7d94f0ca0ea0efdb23b59b6', 307, 'POST', '/admin/online/onlinePage/add', '{\"onlinePageDto\":{\"pageCode\":\"test\",\"pageName\":\"test\",\"pageType\":1,\"status\":1}}', '{\"data\":1809055451229196288,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:43:25'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055625460584448, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineDatasourceController', 'com.orangeforms.common.online.controller.OnlineDatasourceController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'cc233f444b9741e79e85902c6ced44c5', 2989, 'POST', '/admin/online/onlineDatasource/add', '{\"onlineDatasourceDto\":{\"datasourceName\":\"test\",\"dblinkId\":1809055300360081408,\"masterTableName\":\"zz_flow_entry\",\"variableName\":\"test\"},\"pageId\":1809055451229196288}', '{\"data\":1809055636340609024,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:44:06'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055701822083072, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.update', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'b9943033448544c5a5dcb93fe450bbeb', 245, 'POST', '/admin/online/onlinePage/update', '{\"onlinePageDto\":{\"pageCode\":\"test\",\"pageId\":1809055451229196288,\"pageName\":\"test\",\"pageType\":1,\"status\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:44:25'); +INSERT INTO `zz_sys_operation_log` VALUES (1809055740065746944, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'abc44f1518ed471597d38fad6161e8ae', 589, 'POST', '/admin/online/onlineForm/add', '{\"onlineFormDto\":{\"datasourceIdList\":[1809055636340609024],\"formCode\":\"aaa\",\"formKind\":5,\"formName\":\"aaa\",\"formType\":1,\"masterTableId\":1809055626488188928,\"pageId\":1809055451229196288,\"paramsJson\":\"[]\",\"widgetJson\":\"{\\\"pc\\\":{\\\"filterItemWidth\\\":350,\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"tableWidget\\\":{\\\"widgetType\\\":100,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"operationList\\\":[{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"danger\\\",\\\"plain\\\":true,\\\"readOnly\\\":false,\\\"showOrder\\\":0,\\\"eventList\\\":[]},{\\\"id\\\":2,\\\"type\\\":0,\\\"name\\\":\\\"新建\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":false,\\\"readOnly\\\":false,\\\"showOrder\\\":1,\\\"eventList\\\":[]},{\\\"id\\\":3,\\\"type\\\":1,\\\"name\\\":\\\"编辑\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn success\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":10,\\\"eventList\\\":[]},{\\\"id\\\":4,\\\"type\\\":2,\\\"name\\\":\\\"删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn delete\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":15,\\\"eventList\\\":[]}],\\\"showName\\\":\\\"表格组件\\\",\\\"variableName\\\":\\\"table1720147467397\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"paddingBottom\\\":0,\\\"paged\\\":true,\\\"pageSize\\\":10,\\\"operationColumnWidth\\\":160,\\\"tableColumnList\\\":[]},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":true},\\\"leftWidget\\\":{\\\"widgetType\\\":13,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"showName\\\":\\\"树形选择组件\\\",\\\"variableName\\\":\\\"tree1720147467397\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"dictInfo\\\":{},\\\"required\\\":false,\\\"disabled\\\":false},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},\\\"operationList\\\":[{\\\"id\\\":0,\\\"type\\\":3,\\\"name\\\":\\\"导出\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":true,\\\"paramList\\\":[],\\\"eventList\\\":[],\\\"readOnly\\\":false,\\\"showOrder\\\":0},{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"en', '{\"data\":1809055741093351424,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:44:34'); +INSERT INTO `zz_sys_operation_log` VALUES (1809056459653124096, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.delete', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '406cd42f8c3a408fa8928e94a8ebdcc6', 329, 'POST', '/admin/online/onlineForm/delete', '{\"formId\":1809055741093351424}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:47:25'); +INSERT INTO `zz_sys_operation_log` VALUES (1809056484886056960, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.delete', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', 'ac3418b76d474c99862e49acab906011', 76, 'POST', '/admin/online/onlineForm/delete', '{\"formId\":1809055741093351424}', '{\"errorCode\":\"DATA_NOT_EXIST\",\"errorMessage\":\"数据不存在,请刷新后重试!\",\"success\":false}', '192.168.43.167', b'0', '数据不存在,请刷新后重试!', NULL, 1809038124504846336, 'userA', '2024-07-05 10:47:31'); +INSERT INTO `zz_sys_operation_log` VALUES (1809056769645744128, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.add', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '5ac915cc240f4463817134fbcba16d94', 508, 'POST', '/admin/online/onlineForm/add', '{\"onlineFormDto\":{\"datasourceIdList\":[1809055636340609024],\"formCode\":\"aaa\",\"formKind\":5,\"formName\":\"aaa\",\"formType\":1,\"masterTableId\":1809055626488188928,\"pageId\":1809055451229196288,\"paramsJson\":\"[]\",\"widgetJson\":\"{\\\"pc\\\":{\\\"filterItemWidth\\\":350,\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"tableWidget\\\":{\\\"widgetType\\\":100,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"operationList\\\":[{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"danger\\\",\\\"plain\\\":true,\\\"readOnly\\\":false,\\\"showOrder\\\":0,\\\"eventList\\\":[]},{\\\"id\\\":2,\\\"type\\\":0,\\\"name\\\":\\\"新建\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":false,\\\"readOnly\\\":false,\\\"showOrder\\\":1,\\\"eventList\\\":[]},{\\\"id\\\":3,\\\"type\\\":1,\\\"name\\\":\\\"编辑\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn success\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":10,\\\"eventList\\\":[]},{\\\"id\\\":4,\\\"type\\\":2,\\\"name\\\":\\\"删除\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":true,\\\"btnClass\\\":\\\"table-btn delete\\\",\\\"readOnly\\\":false,\\\"showOrder\\\":15,\\\"eventList\\\":[]}],\\\"showName\\\":\\\"表格组件\\\",\\\"variableName\\\":\\\"table1720147715974\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"paddingBottom\\\":0,\\\"paged\\\":true,\\\"pageSize\\\":10,\\\"operationColumnWidth\\\":160,\\\"tableColumnList\\\":[]},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":true},\\\"leftWidget\\\":{\\\"widgetType\\\":13,\\\"bindData\\\":{\\\"defaultValue\\\":{}},\\\"showName\\\":\\\"树形选择组件\\\",\\\"variableName\\\":\\\"tree1720147715974\\\",\\\"props\\\":{\\\"span\\\":24,\\\"height\\\":300,\\\"dictInfo\\\":{},\\\"required\\\":false,\\\"disabled\\\":false},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},\\\"operationList\\\":[{\\\"id\\\":0,\\\"type\\\":3,\\\"name\\\":\\\"导出\\\",\\\"enabled\\\":false,\\\"builtin\\\":true,\\\"rowOperation\\\":false,\\\"btnType\\\":\\\"primary\\\",\\\"plain\\\":true,\\\"paramList\\\":[],\\\"eventList\\\":[],\\\"readOnly\\\":false,\\\"showOrder\\\":0},{\\\"id\\\":1,\\\"type\\\":10,\\\"name\\\":\\\"批量删除\\\",\\\"en', '{\"data\":1809056770480410624,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:48:39'); +INSERT INTO `zz_sys_operation_log` VALUES (1809057010251993088, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.clone', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '32321e03b4ac418ca5d6fea8ac744a09', 453, 'POST', '/admin/online/onlineForm/clone', '{\"formId\":1809056770480410624}', '{\"data\":1809057010814029824,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:49:37'); +INSERT INTO `zz_sys_operation_log` VALUES (1809057028065202176, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.delete', 'Authorization:login:token-session:ae6bfe73-43ea-4a84-a6fb-528e90c339de', '45d08665cef04be4b9a20cc8b9e3505d', 302, 'POST', '/admin/online/onlineForm/delete', '{\"formId\":1809057010814029824}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1809038124504846336, 'userA', '2024-07-05 10:49:41'); +INSERT INTO `zz_sys_operation_log` VALUES (1809131899889651712, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', NULL, '17cfa5afe5374abda116be3f69094fcf', 497, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"ekih%2BFzFR03abVnW2zJLYZJ%2FEHw2EpMZKuW9698GRI6zsXrhLXX1UjKEN11L31%2BrePfnFLvp%2Bk408bZ6CLtfjhTjRR9wbOzPocmtbK063VM%2F7Crw9nAlaSEobYPwWlHuiugw8CcVPPWAAfiSz2yoedg5%2BBbBDx4SnWKKPz7K59Y%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'0', '用户名或密码错误,请重试!', NULL, NULL, NULL, '2024-07-05 15:47:12'); +INSERT INTO `zz_sys_operation_log` VALUES (1809131924610879488, '', 0, 'application-webadmin', 'com.orangeforms.webadmin.upms.controller.LoginController', 'com.orangeforms.webadmin.upms.controller.LoginController.doLogin', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'd2af8f5c698e4e6b8b610163e5908217', 547, 'POST', '/admin/upms/login/doLogin', '{\"password\":\"n1NyIK3vu4fhzT5kFQRSYQvehBUqZ2RK6VOeDT7NKd7Tj7Z78CV6Yg73TdJSKLH7PtQ1yrzCPE7QijTH3CCPqg6x%2FDE0ndlm0GPAmdcG8c1LKu4RrV%2BM37grdKeOtbCbohG4uishREJ9jovLiZI8twfRGCnzqEs3bKBjPybBdDw%3D\",\"loginName\":\"admin\"}', NULL, '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:47:18'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132075907813376, '', 20, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.delete', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f9258eaefc164f9db3a6792c03dbaa82', 1429, 'POST', '/admin/online/onlinePage/delete', '{\"pageId\":1809055451229196288}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:47:54'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132176877293568, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f41bdd39fc984cf38b2cca8ccbefaaa1', 343, 'POST', '/admin/online/onlinePage/add', '{\"onlinePageDto\":{\"pageCode\":\"flowLeave\",\"pageName\":\"请假申请\",\"pageType\":10,\"status\":1}}', '{\"data\":1809132177523216384,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:18'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132250441191424, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineDatasourceController', 'com.orangeforms.common.online.controller.OnlineDatasourceController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '0db3da9d6b6048bab73a3fe445297ae1', 1653, 'POST', '/admin/online/onlineDatasource/add', '{\"onlineDatasourceDto\":{\"datasourceName\":\"请假申请\",\"dblinkId\":1809055300360081408,\"masterTableName\":\"zz_test_flow_leave\",\"variableName\":\"dsLeave\"},\"pageId\":1809132177523216384}', '{\"data\":1809132255981867008,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:35'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132300521181184, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '58819dae9e9f442fb21f5cadb3c5d326', 270, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"请假用户\",\"columnId\":1809132252425097216,\"columnName\":\"user_id\",\"columnShowOrder\":2,\"columnType\":\"bigint\",\"deptFilter\":false,\"fieldKind\":21,\"filterType\":0,\"fullColumnType\":\"bigint\",\"nullable\":false,\"numericPrecision\":19,\"objectFieldName\":\"userId\",\"objectFieldType\":\"Long\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:47'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132348223000576, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '4e7166af1f55482ea6189787d6a661fe', 267, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"开始时间\",\"columnId\":1809132253733720064,\"columnName\":\"leave_begin_time\",\"columnShowOrder\":5,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveBeginTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:48:59'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132364907941888, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '5ea928dfa368402b9bcd8a8259c93097', 256, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"结束时间\",\"columnId\":1809132254102818816,\"columnName\":\"leave_end_time\",\"columnShowOrder\":6,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveEndTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:03'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132399720665088, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'a0c6b421cd5f4f0aa6810db4abe0d301', 683, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"申请时间\",\"columnId\":1809132254388031488,\"columnName\":\"apply_time\",\"columnShowOrder\":7,\"columnType\":\"datetime\",\"deptFilter\":false,\"fieldKind\":20,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"applyTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:11'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132453835575296, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '4a0b9a2c8a614820941b95772b82e6ba', 366, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"最后审批状态\",\"columnId\":1809132254782296064,\"columnName\":\"approval_status\",\"columnShowOrder\":8,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":26,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"approvalStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:24'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132505723310080, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'e54070a44ae2487bb4810a3d920d7988', 1179, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"流程状态\",\"columnId\":1809132255327555584,\"columnName\":\"flow_status\",\"columnShowOrder\":9,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":26,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"flowStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:36'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132536761159680, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '5f0e57d9c5df44a3907bd4878183c68a', 271, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"流程状态\",\"columnId\":1809132255327555584,\"columnName\":\"flow_status\",\"columnShowOrder\":9,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":25,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"flowStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:43'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132559511064576, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '28063f6a43804db882504597d2d77353', 306, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"用户名\",\"columnId\":1809132255679877120,\"columnName\":\"username\",\"columnShowOrder\":10,\"columnType\":\"varchar\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"varchar(255)\",\"nullable\":true,\"objectFieldName\":\"username\",\"objectFieldType\":\"String\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:49'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132570206539776, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '48ce554a01704633a62a3aabbc394b2f', 210, 'POST', '/admin/online/onlinePage/update', '{\"onlinePageDto\":{\"pageCode\":\"flowLeave\",\"pageId\":1809132177523216384,\"pageName\":\"请假申请\",\"pageType\":10,\"status\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:49:51'); +INSERT INTO `zz_sys_operation_log` VALUES (1809132634748489728, '', 10, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '546500c7cc10417d91f89582652f820b', 506, 'POST', '/admin/online/onlineForm/add', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"paramsJson\":\"[]\",\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"customFieldList\\\":[],\\\"widgetList\\\":[],\\\"formEventList\\\":[],\\\"maskFieldList\\\":[],\\\"allowEventList\\\":[\\\"formCreated\\\",\\\"afterLoadFormData\\\",\\\"beforeCommitFormData\\\"],\\\"fullscreen\\\":true,\\\"supportOperation\\\":false,\\\"width\\\":800}}\"}}', '{\"data\":1809132635633487872,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:50:07'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133545088618496, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '413482d6eeac4cdcb05b7423026c3f8d', 306, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"请假类型\",\"columnId\":1809132253377204224,\"columnName\":\"leave_type\",\"columnShowOrder\":4,\"columnType\":\"int\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":false,\"numericPrecision\":10,\"objectFieldName\":\"leaveType\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:44'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133558430699520, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '9dad13d8ce3d4e94b0bf01b544f0c04b', 329, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"请假原因\",\"columnId\":1809132252852916224,\"columnName\":\"leave_reason\",\"columnShowOrder\":3,\"columnType\":\"varchar\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"varchar(512)\",\"nullable\":false,\"objectFieldName\":\"leaveReason\",\"objectFieldType\":\"String\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:47'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133570850033664, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '6573018ce2c547b8ab633c45affb8094', 294, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"开始时间\",\"columnId\":1809132253733720064,\"columnName\":\"leave_begin_time\",\"columnShowOrder\":5,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveBeginTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:50'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133584934506496, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '3c4aad5641704fd991b3b9717d60f039', 386, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"结束时间\",\"columnId\":1809132254102818816,\"columnName\":\"leave_end_time\",\"columnShowOrder\":6,\"columnType\":\"datetime\",\"deptFilter\":false,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"leaveEndTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:53'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133598788292608, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '1fd06e9d5bec413a8e1437968ee99920', 327, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"申请时间\",\"columnId\":1809132254388031488,\"columnName\":\"apply_time\",\"columnShowOrder\":7,\"columnType\":\"datetime\",\"deptFilter\":false,\"fieldKind\":20,\"filterType\":0,\"fullColumnType\":\"datetime\",\"nullable\":false,\"objectFieldName\":\"applyTime\",\"objectFieldType\":\"Date\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:57'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133609777369088, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineColumnController', 'com.orangeforms.common.online.controller.OnlineColumnController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '67104da4ba934ce9904d3c5c646b4836', 281, 'POST', '/admin/online/onlineColumn/update', '{\"onlineColumnDto\":{\"autoIncrement\":false,\"columnComment\":\"最后审批状态\",\"columnId\":1809132254782296064,\"columnName\":\"approval_status\",\"columnShowOrder\":8,\"columnType\":\"int\",\"deptFilter\":false,\"fieldKind\":26,\"filterType\":0,\"fullColumnType\":\"int\",\"nullable\":true,\"numericPrecision\":10,\"objectFieldName\":\"approvalStatus\",\"objectFieldType\":\"Integer\",\"parentKey\":false,\"primaryKey\":false,\"tableId\":1809132251556876288,\"uploadFileSystemType\":0,\"userFilter\":false}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:53:59'); +INSERT INTO `zz_sys_operation_log` VALUES (1809133618182754304, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f3aea6a5c397400eb272d364f6b1870e', 218, 'POST', '/admin/online/onlinePage/update', '{\"onlinePageDto\":{\"pageCode\":\"flowLeave\",\"pageId\":1809132177523216384,\"pageName\":\"请假申请\",\"pageType\":10,\"status\":2}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 15:54:01'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143621992058880, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'ec8cccdd9b5c42e69b2f100bc1307a59', 636, 'POST', '/admin/online/onlineForm/update', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formId\":1809132635633487872,\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"operationList\\\":[],\\\"customFieldList\\\":[],\\\"widgetList\\\":[{\\\"widgetType\\\":1,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132252852916224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假原因\\\",\\\"variableName\\\":\\\"leaveReason\\\",\\\"props\\\":{\\\"span\\\":24,\\\"type\\\":\\\"text\\\",\\\"placeholder\\\":\\\"\\\",\\\"show-password\\\":false,\\\"show-word-limit\\\":false,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false}],\\\"formEventList\\\":[],\\\"maskFieldList\\\":[],\\\"width\\\":800,\\\"fullscreen\\\":true}}\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:33:46'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143691172909056, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'dc39d01f441c439fa6f3e36eaf50529b', 405, 'POST', '/admin/online/onlineForm/update', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formId\":1809132635633487872,\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"operationList\\\":[],\\\"customFieldList\\\":[],\\\"widgetList\\\":[{\\\"widgetType\\\":3,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132253377204224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假类型\\\",\\\"variableName\\\":\\\"leaveType\\\",\\\"props\\\":{\\\"span\\\":24,\\\"placeholder\\\":\\\"\\\",\\\"step\\\":1,\\\"controls\\\":true,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},{\\\"widgetType\\\":1,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132252852916224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假原因\\\",\\\"variableName\\\":\\\"leaveReason\\\",\\\"props\\\":{\\\"span\\\":24,\\\"type\\\":\\\"text\\\",\\\"placeholder\\\":\\\"\\\",\\\"show-password\\\":false,\\\"show-word-limit\\\":false,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{}}],\\\"formEventList\\\":[],\\\"maskFieldList\\\":[],\\\"width\\\":800,\\\"fullscreen\\\":true}}\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:03'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143765026213888, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlineFormController', 'com.orangeforms.common.online.controller.OnlineFormController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '38feac5b0a75448d8557d336ddbbff53', 590, 'POST', '/admin/online/onlineForm/update', '{\"onlineFormDto\":{\"datasourceIdList\":[1809132255981867008],\"formCode\":\"formFlowLeave\",\"formId\":1809132635633487872,\"formKind\":5,\"formName\":\"请假申请\",\"formType\":10,\"masterTableId\":1809132251556876288,\"pageId\":1809132177523216384,\"widgetJson\":\"{\\\"pc\\\":{\\\"gutter\\\":20,\\\"labelWidth\\\":100,\\\"labelPosition\\\":\\\"right\\\",\\\"operationList\\\":[],\\\"customFieldList\\\":[],\\\"widgetList\\\":[{\\\"widgetType\\\":3,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132253377204224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假类型\\\",\\\"variableName\\\":\\\"leaveType\\\",\\\"props\\\":{\\\"span\\\":24,\\\"placeholder\\\":\\\"\\\",\\\"step\\\":1,\\\"controls\\\":true,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{}},{\\\"widgetType\\\":1,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132252852916224\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"请假原因\\\",\\\"variableName\\\":\\\"leaveReason\\\",\\\"props\\\":{\\\"span\\\":24,\\\"type\\\":\\\"text\\\",\\\"placeholder\\\":\\\"\\\",\\\"show-password\\\":false,\\\"show-word-limit\\\":false,\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{}},{\\\"widgetType\\\":20,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132253733720064\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"开始时间\\\",\\\"variableName\\\":\\\"leaveBeginTime\\\",\\\"props\\\":{\\\"span\\\":12,\\\"placeholder\\\":\\\"\\\",\\\"type\\\":\\\"date\\\",\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eventList\\\":[],\\\"childWidgetList\\\":[],\\\"style\\\":{},\\\"supportOperation\\\":false},{\\\"widgetType\\\":20,\\\"bindData\\\":{\\\"defaultValue\\\":{},\\\"tableId\\\":\\\"1809132251556876288\\\",\\\"columnId\\\":\\\"1809132254102818816\\\",\\\"dataType\\\":0},\\\"showName\\\":\\\"结束时间\\\",\\\"variableName\\\":\\\"leaveEndTime\\\",\\\"props\\\":{\\\"span\\\":12,\\\"placeholder\\\":\\\"\\\",\\\"type\\\":\\\"date\\\",\\\"required\\\":true,\\\"disabled\\\":false,\\\"dictInfo\\\":{\\\"paramList\\\":[]},\\\"actions\\\":{}},\\\"eve', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:21'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143792943501312, '', 15, 'application-webadmin', 'com.orangeforms.common.online.controller.OnlinePageController', 'com.orangeforms.common.online.controller.OnlinePageController.updateStatus', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'f373e5187505453b8a73de82f3487b77', 307, 'POST', '/admin/online/onlinePage/updatePublished', '{\"published\":true,\"pageId\":1809132177523216384}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:27'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143848773881856, '', 20, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.delete', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '9a4914d35bed44ad9363c8d9152f09ba', 374, 'POST', '/admin/flow/flowEntry/delete', '{\"entryId\":1809052045395628032}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:34:40'); +INSERT INTO `zz_sys_operation_log` VALUES (1809143990952398848, '', 10, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.add', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '280442aa890542d8b436c40e8b65c3c8', 564, 'POST', '/admin/flow/flowEntry/add', '{\"flowEntryDto\":{\"bindFormType\":0,\"categoryId\":1809051198460792832,\"defaultFormId\":1809132635633487872,\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"LL\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\"}],\\\"notifyTypes\\\":[\\\"email\\\"],\\\"cascadeDeleteBusinessData\\\":true,\\\"supportRevive\\\":false}\",\"pageId\":1809132177523216384,\"processDefinitionKey\":\"flowLeave\",\"processDefinitionName\":\"请假申请\"}}', '{\"data\":1809143991627681792,\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:35:14'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144002897776640, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '059610f822e649e894e551173c416ac0', 249, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":0,\"categoryId\":1809051198460792832,\"defaultFormId\":1809132635633487872,\"diagramType\":0,\"encodedRule\":\"{\\\"middle\\\":\\\"DD\\\",\\\"idWidth\\\":5,\\\"prefix\\\":\\\"LL\\\",\\\"precisionTo\\\":\\\"DAYS\\\",\\\"calculateWhenView\\\":true}\",\"entryId\":1809143991627681792,\"extensionData\":\"{\\\"approvalStatusDict\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_57\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_58\\\"},{\\\"id\\\":3,\\\"name\\\":\\\"驳回\\\",\\\"_X_ROW_KEY\\\":\\\"row_59\\\"},{\\\"id\\\":4,\\\"name\\\":\\\"会签同意\\\",\\\"_X_ROW_KEY\\\":\\\"row_60\\\"},{\\\"id\\\":5,\\\"name\\\":\\\"会签拒绝\\\",\\\"_X_ROW_KEY\\\":\\\"row_61\\\"}],\\\"notifyTypes\\\":[\\\"email\\\"],\\\"cascadeDeleteBusinessData\\\":true,\\\"supportRevive\\\":false}\",\"pageId\":1809132177523216384,\"processDefinitionKey\":\"flowLeave\",\"processDefinitionName\":\"请假申请\"}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:35:17'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144278463549440, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'c0aff90acd7542fc905229d237403dcc', 304, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":0,\"bpmnXml\":\"\\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n ', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:36:35'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144345769545728, '', 15, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.update', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'e16fbd18283347a6bb899a40fac80441', 238, 'POST', '/admin/flow/flowEntry/update', '{\"flowEntryDto\":{\"bindFormType\":0,\"bpmnXml\":\"\\n\\n \\n \\n \\n \\n \\n \\n \\n Flow_0d86buw\\n \\n \\n \\n \\n \\n Flow_1bxwcza\\n \\n \\n \\n \\n \\n \\n \\n \\n ', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:36:39'); +INSERT INTO `zz_sys_operation_log` VALUES (1809144417529892864, '', 65, 'application-webadmin', 'com.orangeforms.common.flow.controller.FlowEntryController', 'com.orangeforms.common.flow.controller.FlowEntryController.publish', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'b71e69dbde6b4f868601d8c9a42f0e03', 3149, 'POST', '/admin/flow/flowEntry/publish', '{\"entryId\":1809143991627681792}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:36:56'); +INSERT INTO `zz_sys_operation_log` VALUES (1809146479772700672, '', 100, 'application-webadmin', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController.startPreview', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', '8cd4a46b301a48de878e676ce5aadd47', 3835, 'POST', '/admin/flow/flowOnlineOperation/startPreview', '{\"masterData\":{\"leave_begin_time\":\"2024-07-05 00:00:00\",\"leave_type\":1,\"leave_reason\":\"111\",\"leave_end_time\":\"2024-07-08 00:00:00\"},\"flowTaskCommentDto\":{\"approvalType\":\"agree\"},\"taskVariableData\":{},\"processDefinitionKey\":\"flowLeave\",\"copyData\":{}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:45:08'); +INSERT INTO `zz_sys_operation_log` VALUES (1809146595745206272, '', 120, 'application-webadmin', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController.submitUserTask', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'eadd3fe01b3244a4826a245ae15c328f', 2462, 'POST', '/admin/flow/flowOnlineOperation/submitUserTask', '{\"processInstanceId\":\"e1fb2ada-3aaa-11ef-86ec-acde48001122\",\"masterData\":{\"leave_begin_time\":\"2024-07-05 00:00:00\",\"leave_type\":1,\"user_id\":\"1808020007993479168\",\"apply_time\":\"2024-07-05 16:45:08\",\"id\":\"1809146480452177920\",\"leave_reason\":\"111\",\"leave_end_time\":\"2024-07-08 00:00:00\"},\"flowTaskCommentDto\":{\"approvalType\":\"agree\",\"taskComment\":\"11\"},\"taskVariableData\":{},\"taskId\":\"e322e20d-3aaa-11ef-86ec-acde48001122\",\"copyData\":{}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:45:35'); +INSERT INTO `zz_sys_operation_log` VALUES (1809146772417679360, '', 120, 'application-webadmin', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController', 'com.orangeforms.common.flow.online.controller.FlowOnlineOperationController.submitUserTask', 'Authorization:login:token-session:9c33d665-b097-42b9-ada3-08b9f2586c94', 'bb806b13041349cc8a12fa2d5de5a028', 2310, 'POST', '/admin/flow/flowOnlineOperation/submitUserTask', '{\"processInstanceId\":\"e1fb2ada-3aaa-11ef-86ec-acde48001122\",\"masterData\":{\"leave_begin_time\":\"2024-07-05 00:00:00\",\"leave_type\":1,\"user_id\":\"1808020007993479168\",\"apply_time\":\"2024-07-05 16:45:08\",\"id\":\"1809146480452177920\",\"leave_reason\":\"111\",\"leave_end_time\":\"2024-07-08 00:00:00\"},\"flowTaskCommentDto\":{\"approvalType\":\"agree\",\"taskComment\":\"44\"},\"taskVariableData\":{},\"taskId\":\"0669cc2a-3aab-11ef-86ec-acde48001122\",\"copyData\":{}}', '{\"errorCode\":\"NO-ERROR\",\"errorMessage\":\"NO-MESSAGE\",\"success\":true}', '192.168.43.167', b'1', NULL, NULL, 1808020007993479168, 'admin', '2024-07-05 16:46:18'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_perm_whitelist +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_perm_whitelist`; +CREATE TABLE `zz_sys_perm_whitelist` ( + `perm_url` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '权限资源的url', + `module_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限资源所属模块名字(通常是Controller的名字)', + `perm_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限的名称', + PRIMARY KEY (`perm_url`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='权限资源白名单表(认证用户均可访问的url资源)'; + +-- ---------------------------- +-- Table structure for zz_sys_post +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_post`; +CREATE TABLE `zz_sys_post` ( + `post_id` bigint NOT NULL COMMENT '岗位Id', + `post_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '岗位名称', + `post_level` int NOT NULL COMMENT '岗位层级,数值越小级别越高', + `leader_post` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否领导岗位', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`post_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_sys_post +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_post` VALUES (1809037927934595072, '领导岗位', 1, b'1', 1808020007993479168, '2024-07-05 09:33:47', 1808020007993479168, '2024-07-05 09:33:47'); +INSERT INTO `zz_sys_post` VALUES (1809037967663042560, '普通员工', 10, b'0', 1808020007993479168, '2024-07-05 09:33:56', 1808020007993479168, '2024-07-05 09:33:56'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_role`; +CREATE TABLE `zz_sys_role` ( + `role_id` bigint NOT NULL COMMENT '主键Id', + `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '角色名称', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + PRIMARY KEY (`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='系统角色表'; + +-- ---------------------------- +-- Records of zz_sys_role +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_role` VALUES (1809037772728569856, '查看全部', 1808020007993479168, '2024-07-05 09:33:10', 1808020007993479168, '2024-07-05 09:33:10'); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_role_menu`; +CREATE TABLE `zz_sys_role_menu` ( + `role_id` bigint NOT NULL COMMENT '角色Id', + `menu_id` bigint NOT NULL COMMENT '菜单Id', + PRIMARY KEY (`role_id`,`menu_id`) USING BTREE, + KEY `idx_menu_id` (`menu_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='角色与菜单对应关系表'; + +-- ---------------------------- +-- Records of zz_sys_role_menu +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1392786476428693504); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1392786549942259712); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1392786950682841088); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418057714138877952); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418057835631087616); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418058289182150656); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418058744037642240); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418059005175009280); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418059167532322816); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1418059283920064512); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1423161217970606080); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1634009076981567488); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020011080486913); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317376); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317377); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317378); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317379); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317380); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317381); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317384); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317385); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020012825317386); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148866); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148867); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148868); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148869); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148870); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148872); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148873); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148874); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148875); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148876); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148877); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148879); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148880); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148881); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148882); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148883); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148884); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148885); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148886); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148887); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148889); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148890); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148891); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148892); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148893); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148894); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148895); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148896); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148897); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148899); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148900); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148901); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148902); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148903); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148905); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148906); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148907); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075098148908); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343171); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343172); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343173); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343174); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343175); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343177); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343179); +INSERT INTO `zz_sys_role_menu` VALUES (1809037772728569856, 1808020075102343180); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_user +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_user`; +CREATE TABLE `zz_sys_user` ( + `user_id` bigint NOT NULL COMMENT '主键Id', + `login_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户登录名称', + `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '密码', + `show_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户显示名称', + `dept_id` bigint NOT NULL COMMENT '用户所在部门Id', + `head_image_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户头像的Url', + `user_type` int NOT NULL COMMENT '用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)', + `user_status` int NOT NULL COMMENT '状态(0: 正常 1: 锁定)', + `email` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户邮箱', + `mobile` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户手机', + `create_user_id` bigint NOT NULL COMMENT '创建者Id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user_id` bigint NOT NULL COMMENT '更新者Id', + `update_time` datetime NOT NULL COMMENT '最后更新时间', + `deleted_flag` int NOT NULL COMMENT '删除标记(1: 正常 -1: 已删除)', + PRIMARY KEY (`user_id`) USING BTREE, + UNIQUE KEY `uk_login_name` (`login_name`) USING BTREE, + KEY `idx_dept_id` (`dept_id`) USING BTREE, + KEY `idx_status` (`user_status`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='系统用户表'; + +-- ---------------------------- +-- Records of zz_sys_user +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_user` VALUES (1808020007993479168, 'admin', '$2a$10$C1/DwnlXP3s.HOFsmL60Resq0juaRt6/WK8JCzcNbgbpueUMs71Um', '管理员', 1808020008341606402, NULL, 0, 0, NULL, NULL, 1808020007993479168, '2024-07-03 00:00:00', 1808020007993479168, '2024-07-03 00:00:00', 1); +INSERT INTO `zz_sys_user` VALUES (1809038124504846336, 'userA', '$2a$10$perpVEYWNTE0.oP0C7L5beiv1EYs3XEn0qkgOKwB8Rm7p/BDGYLEa', '员工A', 1808020008341606402, NULL, 2, 0, NULL, NULL, 1808020007993479168, '2024-07-05 09:34:34', 1809038124504846336, '2024-07-05 10:23:44', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_user_post +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_user_post`; +CREATE TABLE `zz_sys_user_post` ( + `user_id` bigint NOT NULL COMMENT '用户Id', + `dept_post_id` bigint NOT NULL COMMENT '部门岗位Id', + `post_id` bigint NOT NULL COMMENT '岗位Id', + PRIMARY KEY (`user_id`,`dept_post_id`) USING BTREE, + KEY `idx_post_id` (`post_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_sys_user_post +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_user_post` VALUES (1809038124504846336, 1809038003968937984, 1809037967663042560); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_sys_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `zz_sys_user_role`; +CREATE TABLE `zz_sys_user_role` ( + `user_id` bigint NOT NULL COMMENT '用户Id', + `role_id` bigint NOT NULL COMMENT '角色Id', + PRIMARY KEY (`user_id`,`role_id`) USING BTREE, + KEY `idx_role_id` (`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='用户与角色对应关系表'; + +-- ---------------------------- +-- Records of zz_sys_user_role +-- ---------------------------- +BEGIN; +INSERT INTO `zz_sys_user_role` VALUES (1809038124504846336, 1809037772728569856); +COMMIT; + +-- ---------------------------- +-- Table structure for zz_test_flow_leave +-- ---------------------------- +DROP TABLE IF EXISTS `zz_test_flow_leave`; +CREATE TABLE `zz_test_flow_leave` ( + `id` bigint NOT NULL COMMENT '主键Id', + `user_id` bigint NOT NULL COMMENT '请假用户Id', + `leave_reason` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '请假原因', + `leave_type` int NOT NULL COMMENT '请假类型', + `leave_begin_time` datetime NOT NULL COMMENT '请假开始时间', + `leave_end_time` datetime NOT NULL COMMENT '请假结束时间', + `apply_time` datetime NOT NULL COMMENT '申请时间', + `approval_status` int DEFAULT NULL COMMENT '最后审批状态', + `flow_status` int DEFAULT NULL COMMENT '流程状态', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of zz_test_flow_leave +-- ---------------------------- +BEGIN; +INSERT INTO `zz_test_flow_leave` VALUES (1734132261424467969, 1440911410581213417, '测试', 1, '2023-12-11 00:00:00', '2024-01-02 00:00:00', '2023-12-11 16:45:24', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1734132937084899329, 1440911410581213417, '测试', 1, '2023-12-11 00:00:00', '2024-01-10 00:00:00', '2023-12-11 16:48:05', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1734760286021226497, 1440911410581213417, '22', 2, '2023-12-12 00:00:00', '2023-12-14 00:00:00', '2023-12-13 10:20:57', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1735571074717847553, 1440911410581213417, '123', 1, '2023-12-07 00:00:00', '2023-12-08 00:00:00', '2023-12-15 16:02:44', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1735644235845079041, 1440911410581213417, '111', 1, '2023-12-14 00:00:00', '2023-12-16 00:00:00', '2023-12-15 20:53:27', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1735959007710941185, 1440911410581213417, '123123', 2, '2023-12-16 00:00:00', '2023-12-22 00:00:00', '2023-12-16 17:44:15', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736002626216005633, 1440911410581213417, '213213', 1, '2023-12-15 00:00:00', '2024-01-18 00:00:00', '2023-12-16 20:37:34', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736249711238582272, 1440911410581213417, 'qqq', 2, '2023-12-15 00:00:00', '2024-01-17 00:00:00', '2023-12-17 12:59:24', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736653319645958144, 1440911410581213417, '呃呃呃', 1, '2023-12-18 00:00:00', '2023-12-20 00:00:00', '2023-12-18 15:43:12', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1736916738529824769, 1440911410581213417, '请假', 2, '2023-12-21 00:00:00', '2023-12-23 00:00:00', '2023-12-19 09:09:55', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737101008917499905, 1440911410581213417, 'fff', 3, '2023-12-19 00:00:00', '2023-12-20 00:00:00', '2023-12-19 21:22:09', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737314824108380161, 1440911410581213417, '有事', 1, '2023-12-01 00:00:00', '2023-12-09 00:00:00', '2023-12-20 11:31:46', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737358381695373313, 1440911410581213417, '123', 2, '2023-12-13 00:00:00', '2024-01-19 00:00:00', '2023-12-20 14:24:51', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737615175483133953, 1440911410581213417, '尴尬', 1, '2023-12-21 00:00:00', '2023-12-22 00:00:00', '2023-12-21 07:25:16', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737641283461058561, 1440911410581213417, '测试', 1, '2023-12-21 00:00:00', '2023-12-28 00:00:00', '2023-12-21 09:09:00', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737646632062685184, 1440911410581213417, '风复古', 1, '2023-12-22 00:00:00', '2023-12-22 00:00:00', '2023-12-21 09:30:16', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737661659834486784, 1440911410581213417, '想咋就咋', 3, '2023-12-22 00:00:00', '2023-12-22 00:00:00', '2023-12-21 10:29:59', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737662716845232128, 1440911410581213417, '黑胡椒', 1, '2023-12-18 00:00:00', '2023-12-22 00:00:00', '2023-12-21 10:34:11', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666820992667648, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:29', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666823148539905, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:30', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666824016760833, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:30', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737666824809484289, 1440911410581213417, '111', 1, '2023-12-22 00:00:00', '2023-12-20 00:00:00', '2023-12-21 10:50:30', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1737747164756447233, 1440911410581213417, 'c', 1, '2023-12-23 00:00:00', '2024-01-13 00:00:00', '2023-12-21 16:09:45', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738557159815254017, 1440911410581213417, '测试新增', 2, '2023-12-22 00:00:00', '2024-01-12 00:00:00', '2023-12-23 21:48:22', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738586314833399809, 1440911410581213417, '轻机枪', 1, '2023-12-22 00:00:00', '2023-12-29 00:00:00', '2023-12-23 23:44:13', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738590302731505665, 1440911410581213417, '测试', 2, '2023-12-23 00:00:00', '2024-01-04 00:00:00', '2023-12-24 00:00:04', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738593079201370113, 1440911410581213417, '测试', 1, '2024-01-04 00:00:00', '2024-01-11 00:00:00', '2023-12-24 00:11:06', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738597715752783872, 1440911410581213417, '消息 抄送发', 1, '2023-12-13 00:00:00', '2024-01-25 00:00:00', '2023-12-24 00:29:32', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738598397780168705, 1440911410581213417, ' 额', 1, '2023-12-13 00:00:00', '2023-12-13 00:00:00', '2023-12-24 00:32:14', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1738614127170949120, 1440911410581213417, '超市那个', 1, '2023-12-13 00:00:00', '2023-12-24 00:00:00', '2023-12-24 01:34:44', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1739529776575549440, 1440911410581213417, '33232', 1, '2023-12-07 00:00:00', '2024-01-16 00:00:00', '2023-12-26 14:13:12', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1739534951415549952, 1440911410581213417, '111', 1, '2024-01-25 00:00:00', '2024-01-27 00:00:00', '2023-12-26 14:33:46', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1739860694376910849, 1440911410581213417, '111', 1, '2023-12-27 00:00:00', '2023-12-28 00:00:00', '2023-12-27 12:08:09', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1740031035300646913, 1440911410581213417, '测试抄送', 1, '2024-01-03 00:00:00', '2024-01-11 00:00:00', '2023-12-27 23:25:02', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741067028283789313, 1440911410581213417, '测试抄送', 1, '2023-12-29 00:00:00', '2024-02-08 00:00:00', '2023-12-30 20:01:42', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741068565080969217, 1440911410581213417, '亲近抄送', 1, '2024-02-08 00:00:00', '2024-01-19 00:00:00', '2023-12-30 20:07:48', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741075078512119809, 1440911410581213417, '测试抄送', 1, '2023-12-30 00:00:00', '2024-01-26 00:00:00', '2023-12-30 20:33:41', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741077243179831297, 1440911410581213417, '测试抄送', 1, '2023-12-30 00:00:00', '2024-01-12 00:00:00', '2023-12-30 20:42:17', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1741082898645127169, 1440911410581213417, '11111', 1, '2023-12-13 00:00:00', '2023-12-29 00:00:00', '2023-12-30 21:04:45', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1742075427653947392, 1440911410581213417, '6666', 1, '2024-01-02 00:00:00', '2024-01-27 00:00:00', '2024-01-02 14:48:43', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743138899498110977, 1440911410581213417, '2222', 1, '2024-01-10 00:00:00', '2024-01-10 00:00:00', '2024-01-05 13:14:34', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236528957558784, 1440911410581213417, 'dsfsadffsdf', 1, '2024-01-09 00:00:00', '2024-01-31 00:00:00', '2024-01-05 19:42:31', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236847603027968, 1440911410581213417, 'sdfaff', 1, '2024-01-11 00:00:00', '2024-02-06 00:00:00', '2024-01-05 19:43:47', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236894344351745, 1440911410581213417, 'dsfsdfasdf', 1, '2024-01-11 00:00:00', '2024-02-14 00:00:00', '2024-01-05 19:43:58', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743236965743988737, 1440911410581213417, 'zxczxc', 1, '2024-01-20 00:00:00', '2024-02-12 00:00:00', '2024-01-05 19:44:15', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743529562567872512, 1440911410581213417, '休息', 1, '2024-01-12 00:00:00', '2024-01-13 00:00:00', '2024-01-06 15:06:56', NULL, 3, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743570048200478721, 1440911410581213417, '是一款..是,', 1, '2024-01-07 00:00:00', '2024-01-31 00:00:00', '2024-01-06 17:47:48', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743847321545740288, 1440911410581213417, '测试请假', 3, '2024-01-08 00:00:00', '2024-01-24 00:00:00', '2024-01-07 12:09:35', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743848671104995328, 1440911410581213417, '请假新增测试', 1, '2024-01-15 00:00:00', '2024-01-16 00:00:00', '2024-01-07 12:14:57', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1743894439526404097, 1440911410581213417, '测试', 2, '2024-01-07 00:00:00', '2024-01-24 00:00:00', '2024-01-07 15:16:49', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745342183466078208, 1440911410581213417, 'asdfasdf', 1, '2024-01-02 00:00:00', '2024-02-02 00:00:00', '2024-01-11 15:09:38', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745343819995418625, 1440911410581213417, 'adfasd', 1, '2024-01-06 00:00:00', '2024-02-06 00:00:00', '2024-01-11 15:16:08', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745639100335001600, 1440911410581213417, '1234', 1, '2024-01-12 00:00:00', '2024-01-19 00:00:00', '2024-01-12 10:49:29', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1745641568804540417, 1440911410581213417, '123', 1, '2024-01-12 00:00:00', '2024-01-19 00:00:00', '2024-01-12 10:59:17', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1746710995184652289, 1440911410581213417, '11111111111111', 3, '2024-01-16 00:00:00', '2024-01-25 00:00:00', '2024-01-15 09:48:48', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1746821158071701504, 1440911410581213417, 'sfasdf', 1, '2024-02-14 00:00:00', '2024-02-16 00:00:00', '2024-01-15 17:06:33', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1747175673463574529, 1440911410581213417, '1111', 1, '2024-01-16 00:00:00', '2024-01-17 00:00:00', '2024-01-16 16:35:16', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784199563469393920, 1779777400603676672, '111', 1, '2024-04-01 00:00:00', '2024-04-04 00:00:00', '2024-04-27 20:34:59', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784202480981118976, 1779777400603676672, '请假', 1, '2024-04-22 00:00:00', '2024-04-24 00:00:00', '2024-04-27 20:46:35', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784211196795162625, 1779777400603676672, '请假三天', 1, '2024-04-02 00:00:00', '2024-04-05 00:00:00', '2024-04-27 21:21:13', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784221100561928192, 1779777400603676672, '请假出去玩', 1, '2024-04-08 00:00:00', '2024-04-15 00:00:00', '2024-04-27 22:00:34', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1784556947194777601, 1779777400603676672, '111', 1, '2024-04-03 00:00:00', '2024-04-11 00:00:00', '2024-04-28 20:15:06', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1785508179405180928, 1779777400603676672, '11', 1, '2024-05-08 00:00:00', '2024-05-10 00:00:00', '2024-05-01 11:14:58', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787771104035606528, 1779777400603676672, '111', 1, '2024-05-07 00:00:00', '2024-05-08 00:00:00', '2024-05-07 17:07:01', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787771998559014913, 1779777400603676672, '2222', 1, '2024-05-07 00:00:00', '2024-05-15 00:00:00', '2024-05-07 17:10:34', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787817506019217408, 1779777400603676672, '111', 1, '2024-05-08 00:00:00', '2024-05-16 00:00:00', '2024-05-07 20:11:24', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787852380893614081, 1779777400603676672, '1111', 1, '2024-05-14 00:00:00', '2024-05-08 00:00:00', '2024-05-07 22:29:59', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1787853112791273472, 1779777400603676672, '1111', 1, '2024-05-08 00:00:00', '2024-05-16 00:00:00', '2024-05-07 22:32:53', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788107566534889472, 1779777400603676672, '111', 1, '2024-05-08 00:00:00', '2024-05-09 00:00:00', '2024-05-08 15:24:00', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788112135096635392, 1779777400603676672, '111', 1, '2024-05-08 00:00:00', '2024-05-09 00:00:00', '2024-05-08 15:42:09', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788112525678612480, 1779777400603676672, '1111', 2, '2024-05-09 00:00:00', '2024-05-10 00:00:00', '2024-05-08 15:43:42', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1788741582820741120, 1779777400603676672, '秀', 2, '2024-05-07 00:00:00', '2024-05-08 00:00:00', '2024-05-10 09:23:21', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1791767263255203841, 1779777400603676672, '1111', 1, '2024-05-20 00:00:00', '2024-05-21 00:00:00', '2024-05-18 17:46:20', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1792492440158998528, 1779777400603676672, '111222', 2, '2024-05-07 00:00:00', '2024-05-22 00:00:00', '2024-05-20 17:47:55', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1792829634757267456, 1779777400603676672, '1111', 2, '2024-05-14 00:00:00', '2024-05-15 00:00:00', '2024-05-21 16:07:49', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1793489840575090688, 1779777400603676672, '1111', 1, '2024-05-16 00:00:00', '2024-05-24 00:00:00', '2024-05-23 11:51:14', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1795696352311644160, 1779777400603676672, 'dd', 1, '2024-05-02 00:00:00', '2024-05-10 00:00:00', '2024-05-29 13:59:07', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1795992839696420865, 1779777400603676672, 'admin', 1, '2024-05-02 00:00:00', '2024-05-18 00:00:00', '2024-05-30 09:37:16', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1795994391077195776, 1779777400603676672, '1111222', 1, '2024-05-15 00:00:00', '2024-05-16 00:00:00', '2024-05-30 09:43:25', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796109769098924033, 1779777400603676672, '1111', 1, '2024-05-08 00:00:00', '2024-05-10 00:00:00', '2024-05-30 17:21:54', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796110123517612032, 1779777400603676672, '1111222', 1, '2024-05-16 00:00:00', '2024-05-18 00:00:00', '2024-05-30 17:23:18', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796164077765005312, 1779777400603676672, 'admin', 1, '2024-05-10 00:00:00', '2024-06-05 00:00:00', '2024-05-30 20:57:42', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796164941607079936, 1779777400603676672, 'admin', 1, '2024-05-17 00:00:00', '2024-06-12 00:00:00', '2024-05-30 21:01:08', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796173926594777088, 1779777400603676672, 'dd', 1, '2024-05-10 00:00:00', '2024-05-09 00:00:00', '2024-05-30 21:36:50', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796178444468359168, 1779777400603676672, 'x', 1, '2024-05-14 00:00:00', '2024-05-15 00:00:00', '2024-05-30 21:54:47', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796181839363182593, 1779777400603676672, '111', 1, '2024-05-16 00:00:00', '2024-06-18 00:00:00', '2024-05-30 22:08:17', NULL, 4, 'admin'); +INSERT INTO `zz_test_flow_leave` VALUES (1796182559164469249, 1779777400603676672, '4444', 1, '2024-05-08 00:00:00', '2024-05-10 00:00:00', '2024-05-30 22:11:08', NULL, 4, 'admin'); +INSERT INTO `zz_test_flow_leave` VALUES (1796183035536740352, 1779777400603676672, 'dd', 1, '2024-05-18 00:00:00', '2024-05-11 00:00:00', '2024-05-30 22:13:02', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796183248754184192, 1779777400603676672, '11', 1, '2024-05-07 00:00:00', '2024-05-08 00:00:00', '2024-05-30 22:13:53', NULL, 5, 'userTJ2'); +INSERT INTO `zz_test_flow_leave` VALUES (1796185777676226560, 1779777400603676672, 'd', 1, '2024-05-09 00:00:00', '2024-05-09 00:00:00', '2024-05-30 22:23:56', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796187020805017600, 1779777400603676672, 'd', 1, '2024-05-03 00:00:00', '2024-05-03 00:00:00', '2024-05-30 22:28:52', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796188059113361408, 1779777400603676672, 'd', 1, '2024-05-17 00:00:00', '2024-05-17 00:00:00', '2024-05-30 22:33:00', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796188876033757184, 1779777400603676672, 'dd', 1, '2024-05-02 00:00:00', '2024-05-02 00:00:00', '2024-05-30 22:36:14', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796189604152348672, 1779777400603676672, 'dd', 1, '2024-05-03 00:00:00', '2024-05-03 00:00:00', '2024-05-30 22:39:08', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796190467956674560, 1779777400603676672, 'dd', 1, '2024-05-10 00:00:00', '2024-05-16 00:00:00', '2024-05-30 22:42:34', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796191454335340544, 1779777400603676672, 'jk', 1, '2024-05-10 00:00:00', '2024-05-02 00:00:00', '2024-05-30 22:46:29', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796192461547114496, 1779777400603676672, 'd', 1, '2024-05-02 00:00:00', '2024-05-09 00:00:00', '2024-05-30 22:50:29', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796195394187694080, 1779777400603676672, 'dd', 1, '2024-05-03 00:00:00', '2024-05-10 00:00:00', '2024-05-30 23:02:08', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796197180806008832, 1779777400603676672, 'ddd', 1, '2024-05-10 00:00:00', '2024-05-10 00:00:00', '2024-05-30 23:09:14', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796201309611757568, 1779777400603676672, 'dd', 1, '2024-05-17 00:00:00', '2024-05-17 00:00:00', '2024-05-30 23:25:39', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796202010052136960, 1779777400603676672, 'dd', 1, '2024-05-03 00:00:00', '2024-05-03 00:00:00', '2024-05-30 23:28:26', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796204072726958080, 1779777400603676672, 'd', 1, '2024-05-10 00:00:00', '2024-05-10 00:00:00', '2024-05-30 23:36:37', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796354567839944704, 1779777400603676672, 'admin', 1, '2024-05-17 00:00:00', '2024-06-13 00:00:00', '2024-05-31 09:34:38', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796361013583417344, 1779777400603676672, 'admin', 1, '2024-05-11 00:00:00', '2024-06-10 00:00:00', '2024-05-31 10:00:15', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1796442194475749377, 1779777400603676672, 'd', 1, '2024-05-10 00:00:00', '2024-06-06 00:00:00', '2024-05-31 15:22:50', NULL, 4, 'admin'); +INSERT INTO `zz_test_flow_leave` VALUES (1796453212681670656, 1779777400603676672, 'admin', 1, '2024-05-18 00:00:00', '2024-06-10 00:00:00', '2024-05-31 16:06:37', NULL, 4, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1797540170921152512, 1779777400603676672, '111', 1, '2024-06-04 00:00:00', '2024-06-06 00:00:00', '2024-06-03 16:05:48', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1799012020255723520, 1779777400603676672, '1111', 1, '2024-06-12 00:00:00', '2024-06-14 00:00:00', '2024-06-07 17:34:24', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1800522684333821952, 1779777400603676672, '111', 1, '2024-06-12 00:00:00', '2024-06-12 00:00:00', '2024-06-11 21:37:15', NULL, 5, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1800529322327412736, 1799417106157015040, '1111', 1, '2024-06-13 00:00:00', '2024-06-19 00:00:00', '2024-06-11 22:03:37', NULL, NULL, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1807764854329577473, 1779777400603676672, '111', 1, '2024-07-02 00:00:00', '2024-07-11 00:00:00', '2024-07-01 21:15:03', 11, 1, NULL); +INSERT INTO `zz_test_flow_leave` VALUES (1809146480452177920, 1808020007993479168, '111', 1, '2024-07-05 00:00:00', '2024-07-08 00:00:00', '2024-07-05 16:45:08', NULL, NULL, NULL); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/.DS_Store b/OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..09c3fef061132ead2bf8c19660c58b958876e337 GIT binary patch literal 6148 zcmeHKOHRWu5PdFL1i_+9#D-jya1Y+tqlh#K%Lven zG=9!}JjqLHcL2E6Yq#A?GZtEYZtNwwy zDUNu+Gv4ro9cL%?Ki|VMp2UpYM{lhW9(r+|`{J+1#27FJjDZOm&|N3Jn6RdmG6sx+ zzrld)4;fW3SM(9X*1;jP0K|mmAe>7tAvu|1uIMAuLvb;c7*n0L7%ryMo@rdJ=p)8- zIBh /etc/timezone + +# Ubuntu软件源选择中国的服务器 +RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \ No newline at end of file diff --git a/OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/services/redis/redis.conf b/OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/services/redis/redis.conf new file mode 100644 index 00000000..2eecfa5a --- /dev/null +++ b/OrangeFormsOpen-MybatisPlus/zz-resource/docker-files/services/redis/redis.conf @@ -0,0 +1,1307 @@ +# Redis configuration file example. +# +# Note that in order to read the configuration file, Redis must be +# started with the file path as first argument: +# +# ./redis-server /path/to/redis.conf + +# Note on units: when memory size is needed, it is possible to specify +# it in the usual form of 1k 5GB 4M and so forth: +# +# 1k => 1000 bytes +# 1kb => 1024 bytes +# 1m => 1000000 bytes +# 1mb => 1024*1024 bytes +# 1g => 1000000000 bytes +# 1gb => 1024*1024*1024 bytes +# +# units are case insensitive so 1GB 1Gb 1gB are all the same. + +################################## INCLUDES ################################### + +# Include one or more other config files here. This is useful if you +# have a standard template that goes to all Redis servers but also need +# to customize a few per-server settings. Include files can include +# other files, so use this wisely. +# +# Notice option "include" won't be rewritten by command "CONFIG REWRITE" +# from admin or Redis Sentinel. Since Redis always uses the last processed +# line as value of a configuration directive, you'd better put includes +# at the beginning of this file to avoid overwriting config change at runtime. +# +# If instead you are interested in using includes to override configuration +# options, it is better to use include as the last line. +# +# include /path/to/local.conf +# include /path/to/other.conf + +################################## MODULES ##################################### + +# Load modules at startup. If the server is not able to load modules +# it will abort. It is possible to use multiple loadmodule directives. +# +# loadmodule /path/to/my_module.so +# loadmodule /path/to/other_module.so + +################################## NETWORK ##################################### + +# By default, if no "bind" configuration directive is specified, Redis listens +# for connections from all the network interfaces available on the server. +# It is possible to listen to just one or multiple selected interfaces using +# the "bind" configuration directive, followed by one or more IP addresses. +# +# Examples: +# +# bind 192.168.1.100 10.0.0.1 +# bind 127.0.0.1 ::1 +# +# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the +# internet, binding to all the interfaces is dangerous and will expose the +# instance to everybody on the internet. So by default we uncomment the +# following bind directive, that will force Redis to listen only into +# the IPv4 lookback interface address (this means Redis will be able to +# accept connections only from clients running into the same computer it +# is running). +# +# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES +# JUST COMMENT THE FOLLOWING LINE. +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +bind 0.0.0.0 + +# Protected mode is a layer of security protection, in order to avoid that +# Redis instances left open on the internet are accessed and exploited. +# +# When protected mode is on and if: +# +# 1) The server is not binding explicitly to a set of addresses using the +# "bind" directive. +# 2) No password is configured. +# +# The server only accepts connections from clients connecting from the +# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain +# sockets. +# +# By default protected mode is enabled. You should disable it only if +# you are sure you want clients from other hosts to connect to Redis +# even if no authentication is configured, nor a specific set of interfaces +# are explicitly listed using the "bind" directive. +protected-mode yes + +# Accept connections on the specified port, default is 6379 (IANA #815344). +# If port 0 is specified Redis will not listen on a TCP socket. +port 6379 + +# TCP listen() backlog. +# +# In high requests-per-second environments you need an high backlog in order +# to avoid slow clients connections issues. Note that the Linux kernel +# will silently truncate it to the value of /proc/sys/net/core/somaxconn so +# make sure to raise both the value of somaxconn and tcp_max_syn_backlog +# in order to get the desired effect. +tcp-backlog 511 + +# Unix socket. +# +# Specify the path for the Unix socket that will be used to listen for +# incoming connections. There is no default, so Redis will not listen +# on a unix socket when not specified. +# +# unixsocket /tmp/redis.sock +# unixsocketperm 700 + +# Close the connection after a client is idle for N seconds (0 to disable) +timeout 0 + +# TCP keepalive. +# +# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence +# of communication. This is useful for two reasons: +# +# 1) Detect dead peers. +# 2) Take the connection alive from the point of view of network +# equipment in the middle. +# +# On Linux, the specified value (in seconds) is the period used to send ACKs. +# Note that to close the connection the double of the time is needed. +# On other kernels the period depends on the kernel configuration. +# +# A reasonable value for this option is 300 seconds, which is the new +# Redis default starting with Redis 3.2.1. +tcp-keepalive 300 + +################################# GENERAL ##################################### + +# By default Redis does not run as a daemon. Use 'yes' if you need it. +# Note that Redis will write a pid file in /var/run/redis.pid when daemonized. +daemonize no + +# If you run Redis from upstart or systemd, Redis can interact with your +# supervision tree. Options: +# supervised no - no supervision interaction +# supervised upstart - signal upstart by putting Redis into SIGSTOP mode +# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET +# supervised auto - detect upstart or systemd method based on +# UPSTART_JOB or NOTIFY_SOCKET environment variables +# Note: these supervision methods only signal "process is ready." +# They do not enable continuous liveness pings back to your supervisor. +supervised no + +# If a pid file is specified, Redis writes it where specified at startup +# and removes it at exit. +# +# When the server runs non daemonized, no pid file is created if none is +# specified in the configuration. When the server is daemonized, the pid file +# is used even if not specified, defaulting to "/var/run/redis.pid". +# +# Creating a pid file is best effort: if Redis is not able to create it +# nothing bad happens, the server will start and run normally. +pidfile /var/run/redis_6379.pid + +# Specify the server verbosity level. +# This can be one of: +# debug (a lot of information, useful for development/testing) +# verbose (many rarely useful info, but not a mess like the debug level) +# notice (moderately verbose, what you want in production probably) +# warning (only very important / critical messages are logged) +loglevel notice + +# Specify the log file name. Also the empty string can be used to force +# Redis to log on the standard output. Note that if you use standard +# output for logging but daemonize, logs will be sent to /dev/null +logfile /var/log/redis_6379.log + +# To enable logging to the system logger, just set 'syslog-enabled' to yes, +# and optionally update the other syslog parameters to suit your needs. +# syslog-enabled no + +# Specify the syslog identity. +# syslog-ident redis + +# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7. +# syslog-facility local0 + +# Set the number of databases. The default database is DB 0, you can select +# a different one on a per-connection basis using SELECT where +# dbid is a number between 0 and 'databases'-1 +databases 16 + +# By default Redis shows an ASCII art logo only when started to log to the +# standard output and if the standard output is a TTY. Basically this means +# that normally a logo is displayed only in interactive sessions. +# +# However it is possible to force the pre-4.0 behavior and always show a +# ASCII art logo in startup logs by setting the following option to yes. +always-show-logo yes + +################################ SNAPSHOTTING ################################ +# +# Save the DB on disk: +# +# save +# +# Will save the DB if both the given number of seconds and the given +# number of write operations against the DB occurred. +# +# In the example below the behaviour will be to save: +# after 900 sec (15 min) if at least 1 key changed +# after 300 sec (5 min) if at least 10 keys changed +# after 60 sec if at least 10000 keys changed +# +# Note: you can disable saving completely by commenting out all "save" lines. +# +# It is also possible to remove all the previously configured save +# points by adding a save directive with a single empty string argument +# like in the following example: +# +# save "" + +save 900 1 +save 300 10 +save 60 10000 + +# By default Redis will stop accepting writes if RDB snapshots are enabled +# (at least one save point) and the latest background save failed. +# This will make the user aware (in a hard way) that data is not persisting +# on disk properly, otherwise chances are that no one will notice and some +# disaster will happen. +# +# If the background saving process will start working again Redis will +# automatically allow writes again. +# +# However if you have setup your proper monitoring of the Redis server +# and persistence, you may want to disable this feature so that Redis will +# continue to work as usual even if there are problems with disk, +# permissions, and so forth. +stop-writes-on-bgsave-error yes + +# Compress string objects using LZF when dump .rdb databases? +# For default that's set to 'yes' as it's almost always a win. +# If you want to save some CPU in the saving child set it to 'no' but +# the dataset will likely be bigger if you have compressible values or keys. +rdbcompression yes + +# Since version 5 of RDB a CRC64 checksum is placed at the end of the file. +# This makes the format more resistant to corruption but there is a performance +# hit to pay (around 10%) when saving and loading RDB files, so you can disable it +# for maximum performances. +# +# RDB files created with checksum disabled have a checksum of zero that will +# tell the loading code to skip the check. +rdbchecksum yes + +# The filename where to dump the DB +dbfilename dump.rdb + +# The working directory. +# +# The DB will be written inside this directory, with the filename specified +# above using the 'dbfilename' configuration directive. +# +# The Append Only File will also be created inside this directory. +# +# Note that you must specify a directory here, not a file name. +dir ./ + +################################# REPLICATION ################################# + +# Master-Slave replication. Use slaveof to make a Redis instance a copy of +# another Redis server. A few things to understand ASAP about Redis replication. +# +# 1) Redis replication is asynchronous, but you can configure a master to +# stop accepting writes if it appears to be not connected with at least +# a given number of slaves. +# 2) Redis slaves are able to perform a partial resynchronization with the +# master if the replication link is lost for a relatively small amount of +# time. You may want to configure the replication backlog size (see the next +# sections of this file) with a sensible value depending on your needs. +# 3) Replication is automatic and does not need user intervention. After a +# network partition slaves automatically try to reconnect to masters +# and resynchronize with them. +# +# slaveof + +# If the master is password protected (using the "requirepass" configuration +# directive below) it is possible to tell the slave to authenticate before +# starting the replication synchronization process, otherwise the master will +# refuse the slave request. +# +# masterauth + +# When a slave loses its connection with the master, or when the replication +# is still in progress, the slave can act in two different ways: +# +# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will +# still reply to client requests, possibly with out of date data, or the +# data set may just be empty if this is the first synchronization. +# +# 2) if slave-serve-stale-data is set to 'no' the slave will reply with +# an error "SYNC with master in progress" to all the kind of commands +# but to INFO and SLAVEOF. +# +slave-serve-stale-data yes + +# You can configure a slave instance to accept writes or not. Writing against +# a slave instance may be useful to store some ephemeral data (because data +# written on a slave will be easily deleted after resync with the master) but +# may also cause problems if clients are writing to it because of a +# misconfiguration. +# +# Since Redis 2.6 by default slaves are read-only. +# +# Note: read only slaves are not designed to be exposed to untrusted clients +# on the internet. It's just a protection layer against misuse of the instance. +# Still a read only slave exports by default all the administrative commands +# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve +# security of read only slaves using 'rename-command' to shadow all the +# administrative / dangerous commands. +slave-read-only yes + +# Replication SYNC strategy: disk or socket. +# +# ------------------------------------------------------- +# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY +# ------------------------------------------------------- +# +# New slaves and reconnecting slaves that are not able to continue the replication +# process just receiving differences, need to do what is called a "full +# synchronization". An RDB file is transmitted from the master to the slaves. +# The transmission can happen in two different ways: +# +# 1) Disk-backed: The Redis master creates a new process that writes the RDB +# file on disk. Later the file is transferred by the parent +# process to the slaves incrementally. +# 2) Diskless: The Redis master creates a new process that directly writes the +# RDB file to slave sockets, without touching the disk at all. +# +# With disk-backed replication, while the RDB file is generated, more slaves +# can be queued and served with the RDB file as soon as the current child producing +# the RDB file finishes its work. With diskless replication instead once +# the transfer starts, new slaves arriving will be queued and a new transfer +# will start when the current one terminates. +# +# When diskless replication is used, the master waits a configurable amount of +# time (in seconds) before starting the transfer in the hope that multiple slaves +# will arrive and the transfer can be parallelized. +# +# With slow disks and fast (large bandwidth) networks, diskless replication +# works better. +repl-diskless-sync no + +# When diskless replication is enabled, it is possible to configure the delay +# the server waits in order to spawn the child that transfers the RDB via socket +# to the slaves. +# +# This is important since once the transfer starts, it is not possible to serve +# new slaves arriving, that will be queued for the next RDB transfer, so the server +# waits a delay in order to let more slaves arrive. +# +# The delay is specified in seconds, and by default is 5 seconds. To disable +# it entirely just set it to 0 seconds and the transfer will start ASAP. +repl-diskless-sync-delay 5 + +# Slaves send PINGs to server in a predefined interval. It's possible to change +# this interval with the repl_ping_slave_period option. The default value is 10 +# seconds. +# +# repl-ping-slave-period 10 + +# The following option sets the replication timeout for: +# +# 1) Bulk transfer I/O during SYNC, from the point of view of slave. +# 2) Master timeout from the point of view of slaves (data, pings). +# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings). +# +# It is important to make sure that this value is greater than the value +# specified for repl-ping-slave-period otherwise a timeout will be detected +# every time there is low traffic between the master and the slave. +# +# repl-timeout 60 + +# Disable TCP_NODELAY on the slave socket after SYNC? +# +# If you select "yes" Redis will use a smaller number of TCP packets and +# less bandwidth to send data to slaves. But this can add a delay for +# the data to appear on the slave side, up to 40 milliseconds with +# Linux kernels using a default configuration. +# +# If you select "no" the delay for data to appear on the slave side will +# be reduced but more bandwidth will be used for replication. +# +# By default we optimize for low latency, but in very high traffic conditions +# or when the master and slaves are many hops away, turning this to "yes" may +# be a good idea. +repl-disable-tcp-nodelay no + +# Set the replication backlog size. The backlog is a buffer that accumulates +# slave data when slaves are disconnected for some time, so that when a slave +# wants to reconnect again, often a full resync is not needed, but a partial +# resync is enough, just passing the portion of data the slave missed while +# disconnected. +# +# The bigger the replication backlog, the longer the time the slave can be +# disconnected and later be able to perform a partial resynchronization. +# +# The backlog is only allocated once there is at least a slave connected. +# +# repl-backlog-size 1mb + +# After a master has no longer connected slaves for some time, the backlog +# will be freed. The following option configures the amount of seconds that +# need to elapse, starting from the time the last slave disconnected, for +# the backlog buffer to be freed. +# +# Note that slaves never free the backlog for timeout, since they may be +# promoted to masters later, and should be able to correctly "partially +# resynchronize" with the slaves: hence they should always accumulate backlog. +# +# A value of 0 means to never release the backlog. +# +# repl-backlog-ttl 3600 + +# The slave priority is an integer number published by Redis in the INFO output. +# It is used by Redis Sentinel in order to select a slave to promote into a +# master if the master is no longer working correctly. +# +# A slave with a low priority number is considered better for promotion, so +# for instance if there are three slaves with priority 10, 100, 25 Sentinel will +# pick the one with priority 10, that is the lowest. +# +# However a special priority of 0 marks the slave as not able to perform the +# role of master, so a slave with priority of 0 will never be selected by +# Redis Sentinel for promotion. +# +# By default the priority is 100. +slave-priority 100 + +# It is possible for a master to stop accepting writes if there are less than +# N slaves connected, having a lag less or equal than M seconds. +# +# The N slaves need to be in "online" state. +# +# The lag in seconds, that must be <= the specified value, is calculated from +# the last ping received from the slave, that is usually sent every second. +# +# This option does not GUARANTEE that N replicas will accept the write, but +# will limit the window of exposure for lost writes in case not enough slaves +# are available, to the specified number of seconds. +# +# For example to require at least 3 slaves with a lag <= 10 seconds use: +# +# min-slaves-to-write 3 +# min-slaves-max-lag 10 +# +# Setting one or the other to 0 disables the feature. +# +# By default min-slaves-to-write is set to 0 (feature disabled) and +# min-slaves-max-lag is set to 10. + +# A Redis master is able to list the address and port of the attached +# slaves in different ways. For example the "INFO replication" section +# offers this information, which is used, among other tools, by +# Redis Sentinel in order to discover slave instances. +# Another place where this info is available is in the output of the +# "ROLE" command of a master. +# +# The listed IP and address normally reported by a slave is obtained +# in the following way: +# +# IP: The address is auto detected by checking the peer address +# of the socket used by the slave to connect with the master. +# +# Port: The port is communicated by the slave during the replication +# handshake, and is normally the port that the slave is using to +# list for connections. +# +# However when port forwarding or Network Address Translation (NAT) is +# used, the slave may be actually reachable via different IP and port +# pairs. The following two options can be used by a slave in order to +# report to its master a specific set of IP and port, so that both INFO +# and ROLE will report those values. +# +# There is no need to use both the options if you need to override just +# the port or the IP address. +# +# slave-announce-ip 5.5.5.5 +# slave-announce-port 1234 + +################################## SECURITY ################################### + +# Require clients to issue AUTH before processing any other +# commands. This might be useful in environments in which you do not trust +# others with access to the host running redis-server. +# +# This should stay commented out for backward compatibility and because most +# people do not need auth (e.g. they run their own servers). +# +# Warning: since Redis is pretty fast an outside user can try up to +# 150k passwords per second against a good box. This means that you should +# use a very strong password otherwise it will be very easy to break. +# +# requirepass foobared + +# Command renaming. +# +# It is possible to change the name of dangerous commands in a shared +# environment. For instance the CONFIG command may be renamed into something +# hard to guess so that it will still be available for internal-use tools +# but not available for general clients. +# +# Example: +# +# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 +# +# It is also possible to completely kill a command by renaming it into +# an empty string: +# +# rename-command CONFIG "" +# +# Please note that changing the name of commands that are logged into the +# AOF file or transmitted to slaves may cause problems. + +################################### CLIENTS #################################### + +# Set the max number of connected clients at the same time. By default +# this limit is set to 10000 clients, however if the Redis server is not +# able to configure the process file limit to allow for the specified limit +# the max number of allowed clients is set to the current file limit +# minus 32 (as Redis reserves a few file descriptors for internal uses). +# +# Once the limit is reached Redis will close all the new connections sending +# an error 'max number of clients reached'. +# +# maxclients 10000 + +############################## MEMORY MANAGEMENT ################################ + +# Set a memory usage limit to the specified amount of bytes. +# When the memory limit is reached Redis will try to remove keys +# according to the eviction policy selected (see maxmemory-policy). +# +# If Redis can't remove keys according to the policy, or if the policy is +# set to 'noeviction', Redis will start to reply with errors to commands +# that would use more memory, like SET, LPUSH, and so on, and will continue +# to reply to read-only commands like GET. +# +# This option is usually useful when using Redis as an LRU or LFU cache, or to +# set a hard memory limit for an instance (using the 'noeviction' policy). +# +# WARNING: If you have slaves attached to an instance with maxmemory on, +# the size of the output buffers needed to feed the slaves are subtracted +# from the used memory count, so that network problems / resyncs will +# not trigger a loop where keys are evicted, and in turn the output +# buffer of slaves is full with DELs of keys evicted triggering the deletion +# of more keys, and so forth until the database is completely emptied. +# +# In short... if you have slaves attached it is suggested that you set a lower +# limit for maxmemory so that there is some free RAM on the system for slave +# output buffers (but this is not needed if the policy is 'noeviction'). +# +# maxmemory + +# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory +# is reached. You can select among five behaviors: +# +# volatile-lru -> Evict using approximated LRU among the keys with an expire set. +# allkeys-lru -> Evict any key using approximated LRU. +# volatile-lfu -> Evict using approximated LFU among the keys with an expire set. +# allkeys-lfu -> Evict any key using approximated LFU. +# volatile-random -> Remove a random key among the ones with an expire set. +# allkeys-random -> Remove a random key, any key. +# volatile-ttl -> Remove the key with the nearest expire time (minor TTL) +# noeviction -> Don't evict anything, just return an error on write operations. +# +# LRU means Least Recently Used +# LFU means Least Frequently Used +# +# Both LRU, LFU and volatile-ttl are implemented using approximated +# randomized algorithms. +# +# Note: with any of the above policies, Redis will return an error on write +# operations, when there are no suitable keys for eviction. +# +# At the date of writing these commands are: set setnx setex append +# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd +# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby +# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby +# getset mset msetnx exec sort +# +# The default is: +# +# maxmemory-policy noeviction + +# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated +# algorithms (in order to save memory), so you can tune it for speed or +# accuracy. For default Redis will check five keys and pick the one that was +# used less recently, you can change the sample size using the following +# configuration directive. +# +# The default of 5 produces good enough results. 10 Approximates very closely +# true LRU but costs more CPU. 3 is faster but not very accurate. +# +# maxmemory-samples 5 + +############################# LAZY FREEING #################################### + +# Redis has two primitives to delete keys. One is called DEL and is a blocking +# deletion of the object. It means that the server stops processing new commands +# in order to reclaim all the memory associated with an object in a synchronous +# way. If the key deleted is associated with a small object, the time needed +# in order to execute the DEL command is very small and comparable to most other +# O(1) or O(log_N) commands in Redis. However if the key is associated with an +# aggregated value containing millions of elements, the server can block for +# a long time (even seconds) in order to complete the operation. +# +# For the above reasons Redis also offers non blocking deletion primitives +# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and +# FLUSHDB commands, in order to reclaim memory in background. Those commands +# are executed in constant time. Another thread will incrementally free the +# object in the background as fast as possible. +# +# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled. +# It's up to the design of the application to understand when it is a good +# idea to use one or the other. However the Redis server sometimes has to +# delete keys or flush the whole database as a side effect of other operations. +# Specifically Redis deletes objects independently of a user call in the +# following scenarios: +# +# 1) On eviction, because of the maxmemory and maxmemory policy configurations, +# in order to make room for new data, without going over the specified +# memory limit. +# 2) Because of expire: when a key with an associated time to live (see the +# EXPIRE command) must be deleted from memory. +# 3) Because of a side effect of a command that stores data on a key that may +# already exist. For example the RENAME command may delete the old key +# content when it is replaced with another one. Similarly SUNIONSTORE +# or SORT with STORE option may delete existing keys. The SET command +# itself removes any old content of the specified key in order to replace +# it with the specified string. +# 4) During replication, when a slave performs a full resynchronization with +# its master, the content of the whole database is removed in order to +# load the RDB file just transfered. +# +# In all the above cases the default is to delete objects in a blocking way, +# like if DEL was called. However you can configure each case specifically +# in order to instead release memory in a non-blocking way like if UNLINK +# was called, using the following configuration directives: + +lazyfree-lazy-eviction no +lazyfree-lazy-expire no +lazyfree-lazy-server-del no +slave-lazy-flush no + +############################## APPEND ONLY MODE ############################### + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good enough in many applications, but an issue with the Redis process or +# a power outage may result into a few minutes of writes lost (depending on +# the configured save points). +# +# The Append Only File is an alternative persistence mode that provides +# much better durability. For instance using the default data fsync policy +# (see later in the config file) Redis can lose just one second of writes in a +# dramatic event like a server power outage, or a single write if something +# wrong with the Redis process itself happens, but the operating system is +# still running correctly. +# +# AOF and RDB persistence can be enabled at the same time without problems. +# If the AOF is enabled on startup Redis will load the AOF, that is the file +# with the better durability guarantees. +# +# Please check http://redis.io/topics/persistence for more information. + +appendonly no + +# The name of the append only file (default: "appendonly.aof") + +appendfilename "appendonly.aof" + +# The fsync() call tells the Operating System to actually write data on disk +# instead of waiting for more data in the output buffer. Some OS will really flush +# data on disk, some other OS will just try to do it ASAP. +# +# Redis supports three different modes: +# +# no: don't fsync, just let the OS flush the data when it wants. Faster. +# always: fsync after every write to the append only log. Slow, Safest. +# everysec: fsync only one time every second. Compromise. +# +# The default is "everysec", as that's usually the right compromise between +# speed and data safety. It's up to you to understand if you can relax this to +# "no" that will let the operating system flush the output buffer when +# it wants, for better performances (but if you can live with the idea of +# some data loss consider the default persistence mode that's snapshotting), +# or on the contrary, use "always" that's very slow but a bit safer than +# everysec. +# +# More details please check the following article: +# http://antirez.com/post/redis-persistence-demystified.html +# +# If unsure, use "everysec". + +# appendfsync always +appendfsync everysec +# appendfsync no + +# When the AOF fsync policy is set to always or everysec, and a background +# saving process (a background save or AOF log background rewriting) is +# performing a lot of I/O against the disk, in some Linux configurations +# Redis may block too long on the fsync() call. Note that there is no fix for +# this currently, as even performing fsync in a different thread will block +# our synchronous write(2) call. +# +# In order to mitigate this problem it's possible to use the following option +# that will prevent fsync() from being called in the main process while a +# BGSAVE or BGREWRITEAOF is in progress. +# +# This means that while another child is saving, the durability of Redis is +# the same as "appendfsync none". In practical terms, this means that it is +# possible to lose up to 30 seconds of log in the worst scenario (with the +# default Linux settings). +# +# If you have latency problems turn this to "yes". Otherwise leave it as +# "no" that is the safest pick from the point of view of durability. + +no-appendfsync-on-rewrite no + +# Automatic rewrite of the append only file. +# Redis is able to automatically rewrite the log file implicitly calling +# BGREWRITEAOF when the AOF log size grows by the specified percentage. +# +# This is how it works: Redis remembers the size of the AOF file after the +# latest rewrite (if no rewrite has happened since the restart, the size of +# the AOF at startup is used). +# +# This base size is compared to the current size. If the current size is +# bigger than the specified percentage, the rewrite is triggered. Also +# you need to specify a minimal size for the AOF file to be rewritten, this +# is useful to avoid rewriting the AOF file even if the percentage increase +# is reached but it is still pretty small. +# +# Specify a percentage of zero in order to disable the automatic AOF +# rewrite feature. + +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb + +# An AOF file may be found to be truncated at the end during the Redis +# startup process, when the AOF data gets loaded back into memory. +# This may happen when the system where Redis is running +# crashes, especially when an ext4 filesystem is mounted without the +# data=ordered option (however this can't happen when Redis itself +# crashes or aborts but the operating system still works correctly). +# +# Redis can either exit with an error when this happens, or load as much +# data as possible (the default now) and start if the AOF file is found +# to be truncated at the end. The following option controls this behavior. +# +# If aof-load-truncated is set to yes, a truncated AOF file is loaded and +# the Redis server starts emitting a log to inform the user of the event. +# Otherwise if the option is set to no, the server aborts with an error +# and refuses to start. When the option is set to no, the user requires +# to fix the AOF file using the "redis-check-aof" utility before to restart +# the server. +# +# Note that if the AOF file will be found to be corrupted in the middle +# the server will still exit with an error. This option only applies when +# Redis will try to read more data from the AOF file but not enough bytes +# will be found. +aof-load-truncated yes + +# When rewriting the AOF file, Redis is able to use an RDB preamble in the +# AOF file for faster rewrites and recoveries. When this option is turned +# on the rewritten AOF file is composed of two different stanzas: +# +# [RDB file][AOF tail] +# +# When loading Redis recognizes that the AOF file starts with the "REDIS" +# string and loads the prefixed RDB file, and continues loading the AOF +# tail. +# +# This is currently turned off by default in order to avoid the surprise +# of a format change, but will at some point be used as the default. +aof-use-rdb-preamble no + +################################ LUA SCRIPTING ############################### + +# Max execution time of a Lua script in milliseconds. +# +# If the maximum execution time is reached Redis will log that a script is +# still in execution after the maximum allowed time and will start to +# reply to queries with an error. +# +# When a long running script exceeds the maximum execution time only the +# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be +# used to stop a script that did not yet called write commands. The second +# is the only way to shut down the server in the case a write command was +# already issued by the script but the user doesn't want to wait for the natural +# termination of the script. +# +# Set it to 0 or a negative value for unlimited execution without warnings. +lua-time-limit 5000 + +################################ REDIS CLUSTER ############################### +# +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however +# in order to mark it as "mature" we need to wait for a non trivial percentage +# of users to deploy it in production. +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# Normal Redis instances can't be part of a Redis Cluster; only nodes that are +# started as cluster nodes can. In order to start a Redis instance as a +# cluster node enable the cluster support uncommenting the following: +# +# cluster-enabled yes + +# Every cluster node has a cluster configuration file. This file is not +# intended to be edited by hand. It is created and updated by Redis nodes. +# Every Redis Cluster node requires a different cluster configuration file. +# Make sure that instances running in the same system do not have +# overlapping cluster configuration file names. +# +# cluster-config-file nodes-6379.conf + +# Cluster node timeout is the amount of milliseconds a node must be unreachable +# for it to be considered in failure state. +# Most other internal time limits are multiple of the node timeout. +# +# cluster-node-timeout 15000 + +# A slave of a failing master will avoid to start a failover if its data +# looks too old. +# +# There is no simple way for a slave to actually have an exact measure of +# its "data age", so the following two checks are performed: +# +# 1) If there are multiple slaves able to failover, they exchange messages +# in order to try to give an advantage to the slave with the best +# replication offset (more data from the master processed). +# Slaves will try to get their rank by offset, and apply to the start +# of the failover a delay proportional to their rank. +# +# 2) Every single slave computes the time of the last interaction with +# its master. This can be the last ping or command received (if the master +# is still in the "connected" state), or the time that elapsed since the +# disconnection with the master (if the replication link is currently down). +# If the last interaction is too old, the slave will not try to failover +# at all. +# +# The point "2" can be tuned by user. Specifically a slave will not perform +# the failover if, since the last interaction with the master, the time +# elapsed is greater than: +# +# (node-timeout * slave-validity-factor) + repl-ping-slave-period +# +# So for example if node-timeout is 30 seconds, and the slave-validity-factor +# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the +# slave will not try to failover if it was not able to talk with the master +# for longer than 310 seconds. +# +# A large slave-validity-factor may allow slaves with too old data to failover +# a master, while a too small value may prevent the cluster from being able to +# elect a slave at all. +# +# For maximum availability, it is possible to set the slave-validity-factor +# to a value of 0, which means, that slaves will always try to failover the +# master regardless of the last time they interacted with the master. +# (However they'll always try to apply a delay proportional to their +# offset rank). +# +# Zero is the only value able to guarantee that when all the partitions heal +# the cluster will always be able to continue. +# +# cluster-slave-validity-factor 10 + +# Cluster slaves are able to migrate to orphaned masters, that are masters +# that are left without working slaves. This improves the cluster ability +# to resist to failures as otherwise an orphaned master can't be failed over +# in case of failure if it has no working slaves. +# +# Slaves migrate to orphaned masters only if there are still at least a +# given number of other working slaves for their old master. This number +# is the "migration barrier". A migration barrier of 1 means that a slave +# will migrate only if there is at least 1 other working slave for its master +# and so forth. It usually reflects the number of slaves you want for every +# master in your cluster. +# +# Default is 1 (slaves migrate only if their masters remain with at least +# one slave). To disable migration just set it to a very large value. +# A value of 0 can be set but is useful only for debugging and dangerous +# in production. +# +# cluster-migration-barrier 1 + +# By default Redis Cluster nodes stop accepting queries if they detect there +# is at least an hash slot uncovered (no available node is serving it). +# This way if the cluster is partially down (for example a range of hash slots +# are no longer covered) all the cluster becomes, eventually, unavailable. +# It automatically returns available as soon as all the slots are covered again. +# +# However sometimes you want the subset of the cluster which is working, +# to continue to accept queries for the part of the key space that is still +# covered. In order to do so, just set the cluster-require-full-coverage +# option to no. +# +# cluster-require-full-coverage yes + +# In order to setup your cluster make sure to read the documentation +# available at http://redis.io web site. + +########################## CLUSTER DOCKER/NAT support ######################## + +# In certain deployments, Redis Cluster nodes address discovery fails, because +# addresses are NAT-ted or because ports are forwarded (the typical case is +# Docker and other containers). +# +# In order to make Redis Cluster working in such environments, a static +# configuration where each node knows its public address is needed. The +# following two options are used for this scope, and are: +# +# * cluster-announce-ip +# * cluster-announce-port +# * cluster-announce-bus-port +# +# Each instruct the node about its address, client port, and cluster message +# bus port. The information is then published in the header of the bus packets +# so that other nodes will be able to correctly map the address of the node +# publishing the information. +# +# If the above options are not used, the normal Redis Cluster auto-detection +# will be used instead. +# +# Note that when remapped, the bus port may not be at the fixed offset of +# clients port + 10000, so you can specify any port and bus-port depending +# on how they get remapped. If the bus-port is not set, a fixed offset of +# 10000 will be used as usually. +# +# Example: +# +# cluster-announce-ip 10.1.1.5 +# cluster-announce-port 6379 +# cluster-announce-bus-port 6380 + +################################## SLOW LOG ################################### + +# The Redis Slow Log is a system to log queries that exceeded a specified +# execution time. The execution time does not include the I/O operations +# like talking with the client, sending the reply and so forth, +# but just the time needed to actually execute the command (this is the only +# stage of command execution where the thread is blocked and can not serve +# other requests in the meantime). +# +# You can configure the slow log with two parameters: one tells Redis +# what is the execution time, in microseconds, to exceed in order for the +# command to get logged, and the other parameter is the length of the +# slow log. When a new command is logged the oldest one is removed from the +# queue of logged commands. + +# The following time is expressed in microseconds, so 1000000 is equivalent +# to one second. Note that a negative number disables the slow log, while +# a value of zero forces the logging of every command. +slowlog-log-slower-than 10000 + +# There is no limit to this length. Just be aware that it will consume memory. +# You can reclaim memory used by the slow log with SLOWLOG RESET. +slowlog-max-len 128 + +################################ LATENCY MONITOR ############################## + +# The Redis latency monitoring subsystem samples different operations +# at runtime in order to collect data related to possible sources of +# latency of a Redis instance. +# +# Via the LATENCY command this information is available to the user that can +# print graphs and obtain reports. +# +# The system only logs operations that were performed in a time equal or +# greater than the amount of milliseconds specified via the +# latency-monitor-threshold configuration directive. When its value is set +# to zero, the latency monitor is turned off. +# +# By default latency monitoring is disabled since it is mostly not needed +# if you don't have latency issues, and collecting data has a performance +# impact, that while very small, can be measured under big load. Latency +# monitoring can easily be enabled at runtime using the command +# "CONFIG SET latency-monitor-threshold " if needed. +latency-monitor-threshold 0 + +############################# EVENT NOTIFICATION ############################## + +# Redis can notify Pub/Sub clients about events happening in the key space. +# This feature is documented at http://redis.io/topics/notifications +# +# For instance if keyspace events notification is enabled, and a client +# performs a DEL operation on key "foo" stored in the Database 0, two +# messages will be published via Pub/Sub: +# +# PUBLISH __keyspace@0__:foo del +# PUBLISH __keyevent@0__:del foo +# +# It is possible to select the events that Redis will notify among a set +# of classes. Every class is identified by a single character: +# +# K Keyspace events, published with __keyspace@__ prefix. +# E Keyevent events, published with __keyevent@__ prefix. +# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ... +# $ String commands +# l List commands +# s Set commands +# h Hash commands +# z Sorted set commands +# x Expired events (events generated every time a key expires) +# e Evicted events (events generated when a key is evicted for maxmemory) +# A Alias for g$lshzxe, so that the "AKE" string means all the events. +# +# The "notify-keyspace-events" takes as argument a string that is composed +# of zero or multiple characters. The empty string means that notifications +# are disabled. +# +# Example: to enable list and generic events, from the point of view of the +# event name, use: +# +# notify-keyspace-events Elg +# +# Example 2: to get the stream of the expired keys subscribing to channel +# name __keyevent@0__:expired use: +# +# notify-keyspace-events Ex +# +# By default all notifications are disabled because most users don't need +# this feature and the feature has some overhead. Note that if you don't +# specify at least one of K or E, no events will be delivered. +notify-keyspace-events "" + +############################### ADVANCED CONFIG ############################### + +# Hashes are encoded using a memory efficient data structure when they have a +# small number of entries, and the biggest entry does not exceed a given +# threshold. These thresholds can be configured using the following directives. +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 + +# Lists are also encoded in a special way to save a lot of space. +# The number of entries allowed per internal list node can be specified +# as a fixed maximum size or a maximum number of elements. +# For a fixed maximum size, use -5 through -1, meaning: +# -5: max size: 64 Kb <-- not recommended for normal workloads +# -4: max size: 32 Kb <-- not recommended +# -3: max size: 16 Kb <-- probably not recommended +# -2: max size: 8 Kb <-- good +# -1: max size: 4 Kb <-- good +# Positive numbers mean store up to _exactly_ that number of elements +# per list node. +# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size), +# but if your use case is unique, adjust the settings as necessary. +list-max-ziplist-size -2 + +# Lists may also be compressed. +# Compress depth is the number of quicklist ziplist nodes from *each* side of +# the list to *exclude* from compression. The head and tail of the list +# are always uncompressed for fast push/pop operations. Settings are: +# 0: disable all list compression +# 1: depth 1 means "don't start compressing until after 1 node into the list, +# going from either the head or tail" +# So: [head]->node->node->...->node->[tail] +# [head], [tail] will always be uncompressed; inner nodes will compress. +# 2: [head]->[next]->node->node->...->node->[prev]->[tail] +# 2 here means: don't compress head or head->next or tail->prev or tail, +# but compress all nodes between them. +# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail] +# etc. +list-compress-depth 0 + +# Sets have a special encoding in just one case: when a set is composed +# of just strings that happen to be integers in radix 10 in the range +# of 64 bit signed integers. +# The following configuration setting sets the limit in the size of the +# set in order to use this special memory saving encoding. +set-max-intset-entries 512 + +# Similarly to hashes and lists, sorted sets are also specially encoded in +# order to save a lot of space. This encoding is only used when the length and +# elements of a sorted set are below the following limits: +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 + +# HyperLogLog sparse representation bytes limit. The limit includes the +# 16 bytes header. When an HyperLogLog using the sparse representation crosses +# this limit, it is converted into the dense representation. +# +# A value greater than 16000 is totally useless, since at that point the +# dense representation is more memory efficient. +# +# The suggested value is ~ 3000 in order to have the benefits of +# the space efficient encoding without slowing down too much PFADD, +# which is O(N) with the sparse encoding. The value can be raised to +# ~ 10000 when CPU is not a concern, but space is, and the data set is +# composed of many HyperLogLogs with cardinality in the 0 - 15000 range. +hll-sparse-max-bytes 3000 + +# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in +# order to help rehashing the main Redis hash table (the one mapping top-level +# keys to values). The hash table implementation Redis uses (see dict.c) +# performs a lazy rehashing: the more operation you run into a hash table +# that is rehashing, the more rehashing "steps" are performed, so if the +# server is idle the rehashing is never complete and some more memory is used +# by the hash table. +# +# The default is to use this millisecond 10 times every second in order to +# actively rehash the main dictionaries, freeing memory when possible. +# +# If unsure: +# use "activerehashing no" if you have hard latency requirements and it is +# not a good thing in your environment that Redis can reply from time to time +# to queries with 2 milliseconds delay. +# +# use "activerehashing yes" if you don't have such hard requirements but +# want to free memory asap when possible. +activerehashing yes + +# The client output buffer limits can be used to force disconnection of clients +# that are not reading data from the server fast enough for some reason (a +# common reason is that a Pub/Sub client can't consume messages as fast as the +# publisher can produce them). +# +# The limit can be set differently for the three different classes of clients: +# +# normal -> normal clients including MONITOR clients +# slave -> slave clients +# pubsub -> clients subscribed to at least one pubsub channel or pattern +# +# The syntax of every client-output-buffer-limit directive is the following: +# +# client-output-buffer-limit +# +# A client is immediately disconnected once the hard limit is reached, or if +# the soft limit is reached and remains reached for the specified number of +# seconds (continuously). +# So for instance if the hard limit is 32 megabytes and the soft limit is +# 16 megabytes / 10 seconds, the client will get disconnected immediately +# if the size of the output buffers reach 32 megabytes, but will also get +# disconnected if the client reaches 16 megabytes and continuously overcomes +# the limit for 10 seconds. +# +# By default normal clients are not limited because they don't receive data +# without asking (in a push way), but just after a request, so only +# asynchronous clients may create a scenario where data is requested faster +# than it can read. +# +# Instead there is a default limit for pubsub and slave clients, since +# subscribers and slaves receive data in a push fashion. +# +# Both the hard or the soft limit can be disabled by setting them to zero. +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 + +# Client query buffers accumulate new commands. They are limited to a fixed +# amount by default in order to avoid that a protocol desynchronization (for +# instance due to a bug in the client) will lead to unbound memory usage in +# the query buffer. However you can configure it here if you have very special +# needs, such us huge multi/exec requests or alike. +# +# client-query-buffer-limit 1gb + +# In the Redis protocol, bulk requests, that are, elements representing single +# strings, are normally limited ot 512 mb. However you can change this limit +# here. +# +# proto-max-bulk-len 512mb + +# Redis calls an internal function to perform many background tasks, like +# closing connections of clients in timeout, purging expired keys that are +# never requested, and so forth. +# +# Not all tasks are performed with the same frequency, but Redis checks for +# tasks to perform according to the specified "hz" value. +# +# By default "hz" is set to 10. Raising the value will use more CPU when +# Redis is idle, but at the same time will make Redis more responsive when +# there are many keys expiring at the same time, and timeouts may be +# handled with more precision. +# +# The range is between 1 and 500, however a value over 100 is usually not +# a good idea. Most users should use the default of 10 and raise this up to +# 100 only in environments where very low latency is required. +hz 10 + +# When a child rewrites the AOF file, if the following option is enabled +# the file will be fsync-ed every 32 MB of data generated. This is useful +# in order to commit the file to the disk more incrementally and avoid +# big latency spikes. +aof-rewrite-incremental-fsync yes + +# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good +# idea to start with the default settings and only change them after investigating +# how to improve the performances and how the keys LFU change over time, which +# is possible to inspect via the OBJECT FREQ command. +# +# There are two tunable parameters in the Redis LFU implementation: the +# counter logarithm factor and the counter decay time. It is important to +# understand what the two parameters mean before changing them. +# +# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis +# uses a probabilistic increment with logarithmic behavior. Given the value +# of the old counter, when a key is accessed, the counter is incremented in +# this way: +# +# 1. A random number R between 0 and 1 is extracted. +# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1). +# 3. The counter is incremented only if R < P. +# +# The default lfu-log-factor is 10. This is a table of how the frequency +# counter changes with a different number of accesses with different +# logarithmic factors: +# +# +--------+------------+------------+------------+------------+------------+ +# | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits | +# +--------+------------+------------+------------+------------+------------+ +# | 0 | 104 | 255 | 255 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 1 | 18 | 49 | 255 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 10 | 10 | 18 | 142 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 100 | 8 | 11 | 49 | 143 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# +# NOTE: The above table was obtained by running the following commands: +# +# redis-benchmark -n 1000000 incr foo +# redis-cli object freq foo +# +# NOTE 2: The counter initial value is 5 in order to give new objects a chance +# to accumulate hits. +# +# The counter decay time is the time, in minutes, that must elapse in order +# for the key counter to be divided by two (or decremented if it has a value +# less <= 10). +# +# The default value for the lfu-decay-time is 1. A Special value of 0 means to +# decay the counter every time it happens to be scanned. +# +# lfu-log-factor 10 +# lfu-decay-time 1 + +########################### ACTIVE DEFRAGMENTATION ####################### +# +# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested +# even in production and manually tested by multiple engineers for some +# time. +# +# What is active defragmentation? +# ------------------------------- +# +# Active (online) defragmentation allows a Redis server to compact the +# spaces left between small allocations and deallocations of data in memory, +# thus allowing to reclaim back memory. +# +# Fragmentation is a natural process that happens with every allocator (but +# less so with Jemalloc, fortunately) and certain workloads. Normally a server +# restart is needed in order to lower the fragmentation, or at least to flush +# away all the data and create it again. However thanks to this feature +# implemented by Oran Agra for Redis 4.0 this process can happen at runtime +# in an "hot" way, while the server is running. +# +# Basically when the fragmentation is over a certain level (see the +# configuration options below) Redis will start to create new copies of the +# values in contiguous memory regions by exploiting certain specific Jemalloc +# features (in order to understand if an allocation is causing fragmentation +# and to allocate it in a better place), and at the same time, will release the +# old copies of the data. This process, repeated incrementally for all the keys +# will cause the fragmentation to drop back to normal values. +# +# Important things to understand: +# +# 1. This feature is disabled by default, and only works if you compiled Redis +# to use the copy of Jemalloc we ship with the source code of Redis. +# This is the default with Linux builds. +# +# 2. You never need to enable this feature if you don't have fragmentation +# issues. +# +# 3. Once you experience fragmentation, you can enable this feature when +# needed with the command "CONFIG SET activedefrag yes". +# +# The configuration parameters are able to fine tune the behavior of the +# defragmentation process. If you are not sure about what they mean it is +# a good idea to leave the defaults untouched. + +# Enabled active defragmentation +# activedefrag yes + +# Minimum amount of fragmentation waste to start active defrag +# active-defrag-ignore-bytes 100mb + +# Minimum percentage of fragmentation to start active defrag +# active-defrag-threshold-lower 10 + +# Maximum percentage of fragmentation at which we use maximum effort +# active-defrag-threshold-upper 100 + +# Minimal effort for defrag in CPU percentage +# active-defrag-cycle-min 25 + +# Maximal effort for defrag in CPU percentage +# active-defrag-cycle-max 75 + diff --git a/OrangeFormsOpen-VUE3/.editorconfig b/OrangeFormsOpen-VUE3/.editorconfig new file mode 100644 index 00000000..c61b6c7c --- /dev/null +++ b/OrangeFormsOpen-VUE3/.editorconfig @@ -0,0 +1,24 @@ +# editorconfig.org +#项目里读editorcongig文件时,读到此文件即可,不用继续往上搜索 +root = true + +# 规范指定:全部文件 +[*] +# 文档的字符编码:使用UTF-8 - Unicode 字符编码; +# 常见的字符编码有两种:1.UTF-8 - Unicode 字符编码;2.ISO-8859-1 - 拉丁字母表的字符编码。 +charset = utf-8 +#tab类型:空格(不使用制表符) +indent_style = space +#tab键长度是两个空格 +indent_size = 2 +#跟系统有关。win用cr lf,linux/unix用lf,mac用cr。 +end_of_line = lf +#保存文件时,在最后一行后加上一行空行 +insert_final_newline = true +#去掉每行代码最后多余的空格 +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false + diff --git a/OrangeFormsOpen-VUE3/.env.development b/OrangeFormsOpen-VUE3/.env.development new file mode 100644 index 00000000..aec1dbb3 --- /dev/null +++ b/OrangeFormsOpen-VUE3/.env.development @@ -0,0 +1,2 @@ +VITE_SERVER_HOST='http://localhost:8082/' +VITE_PROJECT_NAME='橙单演示工程' diff --git a/OrangeFormsOpen-VUE3/.env.production b/OrangeFormsOpen-VUE3/.env.production new file mode 100644 index 00000000..1db60839 --- /dev/null +++ b/OrangeFormsOpen-VUE3/.env.production @@ -0,0 +1,2 @@ +VITE_SERVER_HOST='http://localhost:8082/' +VITE_PROJECT_NAME='橙单项目' \ No newline at end of file diff --git a/OrangeFormsOpen-VUE3/.eslintignore b/OrangeFormsOpen-VUE3/.eslintignore new file mode 100644 index 00000000..5e6a1d9b --- /dev/null +++ b/OrangeFormsOpen-VUE3/.eslintignore @@ -0,0 +1,7 @@ +node_modules +dist/ +test +src/vite-env.d.ts +/src/pages/workflow/package/* +/src/components/Verifition/* +/src/components/SpreadSheet/* \ No newline at end of file diff --git a/OrangeFormsOpen-VUE3/.eslintrc-auto-import.json b/OrangeFormsOpen-VUE3/.eslintrc-auto-import.json new file mode 100644 index 00000000..2639e71a --- /dev/null +++ b/OrangeFormsOpen-VUE3/.eslintrc-auto-import.json @@ -0,0 +1,68 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": true, + "WritableComputedRef": true, + "computed": true, + "createApp": true, + "customRef": true, + "defineAsyncComponent": true, + "defineComponent": true, + "effectScope": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "inject": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onUnmounted": true, + "onUpdated": true, + "provide": true, + "reactive": true, + "readonly": true, + "ref": true, + "resolveComponent": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "toRaw": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "unref": true, + "useAttrs": true, + "useCssModule": true, + "useCssVars": true, + "useSlots": true, + "watch": true, + "watchEffect": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "showDialog": true + } +} diff --git a/OrangeFormsOpen-VUE3/.eslintrc.cjs b/OrangeFormsOpen-VUE3/.eslintrc.cjs new file mode 100644 index 00000000..d9cfddb8 --- /dev/null +++ b/OrangeFormsOpen-VUE3/.eslintrc.cjs @@ -0,0 +1,67 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + './.eslintrc-auto-import.json', + 'eslint:recommended', + 'plugin:vue/vue3-essential', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/typescript', + 'plugin:import/recommended', + 'plugin:prettier/recommended', + ], + settings: { + node: { + extensions: ['.ts', '.tsx'], + moduleDirectory: ['node_modules', 'src'], + }, + 'import/resolver': { + typescript: {}, + }, + }, + overrides: [ + //这里是添加的代码 + { + files: [ + 'src/pages/**/*.vue', + 'src/pages/**/**/*.vue', + 'src/components/**/*.vue', + 'src/components/**/**/index.vue', + ], // 匹配views和二级目录中的index.vue + rules: { + 'vue/multi-word-component-names': 'off', + }, //给上面匹配的文件指定规则 + }, + ], + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'prettier/prettier': 'error', + 'linebreak-style': ['error', 'unix'], + 'vue/comment-directive': 'off', + 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'import/extensions': [ + 'error', + 'ignorePackages', + { + ts: 'never', + tsx: 'never', + }, + ], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], + }, + ], + }, +}; diff --git a/OrangeFormsOpen-VUE3/.gitignore b/OrangeFormsOpen-VUE3/.gitignore new file mode 100644 index 00000000..727148ad --- /dev/null +++ b/OrangeFormsOpen-VUE3/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.stylelintcache +.DS_Store diff --git a/OrangeFormsOpen-VUE3/.prettierrc.cjs b/OrangeFormsOpen-VUE3/.prettierrc.cjs new file mode 100644 index 00000000..93cbb7a8 --- /dev/null +++ b/OrangeFormsOpen-VUE3/.prettierrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + printWidth: 100, // 每行代码长度(默认80) + tabWidth: 2, // 每个tab相当于多少个空格(默认2) + useTabs: false, // 是否使用tab进行缩进(默认false) + singleQuote: true, // 使用单引号(默认false) + semi: true, // 声明结尾使用分号(默认true) + trailingComma: 'all', // 多行使用拖尾逗号(默认none) + bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true) + jsxBracketSameLine: false, // 多行JSX中的>放置在最后一行的结尾,而不是另起一行(默认false) + arrowParens: 'avoid', // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid) + jsxBracketSameLine: false, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "html" + } + } + ] +}; diff --git a/OrangeFormsOpen-VUE3/.vscode/settings.json b/OrangeFormsOpen-VUE3/.vscode/settings.json new file mode 100644 index 00000000..3fe7bd98 --- /dev/null +++ b/OrangeFormsOpen-VUE3/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll.stylelint": "explicit" + }, + "eslint.validate": ["javascript", "vue", "html", "typescript"], + "css.validate": false, + "scss.validate": false, + "stylelint.validate": ["css", "postcss", "scss", "vue", "sass"] +} diff --git a/OrangeFormsOpen-VUE3/README.md b/OrangeFormsOpen-VUE3/README.md new file mode 100644 index 00000000..34b74b6a --- /dev/null +++ b/OrangeFormsOpen-VUE3/README.md @@ -0,0 +1,25 @@ +# 探索前端前沿技术,并创建相关的模板 + +## 主要内容 + +### 主要工具包 + +* vite4 vue3 pinia vue-router axios ts element-plus vant + +### 主要规范工化工具 + +* eslint规范及配置 +* prettier规范及配置 +* stylelint规范及配置 + +### 推荐的约束 + +* 推荐使用vscode编辑器 安装eslint插件 prettier插件 Volar插件 +* 推荐使用node 18.16.1 版本 + +### 使用方式 + +* 克隆项目到本地 +* 进入项目根目录 执行 npm install 安装依赖 +* 启动项目 npm run dev +* 访问 diff --git a/OrangeFormsOpen-VUE3/components.d.ts b/OrangeFormsOpen-VUE3/components.d.ts new file mode 100644 index 00000000..2be5a445 --- /dev/null +++ b/OrangeFormsOpen-VUE3/components.d.ts @@ -0,0 +1,141 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + AdvanceQuery: typeof import('./src/components/AdvanceQuery/index.vue')['default'] + BarChart: typeof import('./src/components/Charts/barChart.vue')['default'] + Base: typeof import('./src/components/Charts/base.vue')['default'] + BreadCrumb: typeof import('./src/components/layout/components/BreadCrumb.vue')['default'] + CarouselChart: typeof import('./src/components/Charts/carouselChart.vue')['default'] + CommonList: typeof import('./src/components/Charts/commonList.vue')['default'] + DataCard: typeof import('./src/components/Charts/dataCard.vue')['default'] + DataProgressCard: typeof import('./src/components/Charts/dataProgressCard.vue')['default'] + DataViewTable: typeof import('./src/components/Charts/dataViewTable.vue')['default'] + DateRange: typeof import('./src/components/DateRange/index.vue')['default'] + DeptSelect: typeof import('./src/components/DeptSelect/index.vue')['default'] + DeptSelectDlg: typeof import('./src/components/DeptSelect/DeptSelectDlg.vue')['default'] + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElAside: typeof import('element-plus/es')['ElAside'] + ElBadge: typeof import('element-plus/es')['ElBadge'] + ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] + ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCarousel: typeof import('element-plus/es')['ElCarousel'] + ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] + ElCascader: typeof import('element-plus/es')['ElCascader'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElCollapse: typeof import('element-plus/es')['ElCollapse'] + ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] + ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] + ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] + ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElFooter: typeof import('element-plus/es')['ElFooter'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElHeader: typeof import('element-plus/es')['ElHeader'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElIconArrowDown: typeof import('@element-plus/icons-vue')['ArrowDown'] + ElIconArrowLeft: typeof import('@element-plus/icons-vue')['ArrowLeft'] + ElIconArrowRight: typeof import('@element-plus/icons-vue')['ArrowRight'] + ElIconCaretBottom: typeof import('@element-plus/icons-vue')['CaretBottom'] + ElIconClose: typeof import('@element-plus/icons-vue')['Close'] + ElImage: typeof import('element-plus/es')['ElImage'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElLink: typeof import('element-plus/es')['ElLink'] + ElMain: typeof import('element-plus/es')['ElMain'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSlider: typeof import('element-plus/es')['ElSlider'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTree: typeof import('element-plus/es')['ElTree'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + FilterBox: typeof import('./src/components/FilterBox/index.vue')['default'] + FunnelChart: typeof import('./src/components/Charts/funnelChart.vue')['default'] + FunnelChartV3: typeof import('./src/components/Charts/funnelChartV3.vue')['default'] + GaugeChart: typeof import('./src/components/Charts/gaugeChart.vue')['default'] + Icons: typeof import('./src/components/icons/index.vue')['default'] + IconSelect: typeof import('./src/components/IconSelect/index.vue')['default'] + InputNumberRange: typeof import('./src/components/InputNumberRange/index.vue')['default'] + Layout: typeof import('./src/components/Dialog/layout.vue')['default'] + LineChart: typeof import('./src/components/Charts/lineChart.vue')['default'] + MultiColumn: typeof import('./src/components/layout/components/multi-column.vue')['default'] + MultiColumnMenu: typeof import('./src/components/layout/components/multi-column-menu.vue')['default'] + MultiItemBox: typeof import('./src/components/MultiItemBox/index.vue')['default'] + MultiItemList: typeof import('./src/components/MultiItemList/index.vue')['default'] + PageCloseButton: typeof import('./src/components/PageCloseButton/index.vue')['default'] + PieChart: typeof import('./src/components/Charts/pieChart.vue')['default'] + PivotTable: typeof import('./src/components/Charts/pivotTable.vue')['default'] + PivotTableColumn: typeof import('./src/components/Charts/pivotTableColumn.vue')['default'] + Progress: typeof import('./src/components/Progress/index.vue')['default'] + ProgressBar: typeof import('./src/components/Charts/progressBar.vue')['default'] + ProgressCircle: typeof import('./src/components/Charts/progressCircle.vue')['default'] + RadarChart: typeof import('./src/components/Charts/radarChart.vue')['default'] + RadarChartV3: typeof import('./src/components/Charts/radarChartV3.vue')['default'] + RichEditor: typeof import('./src/components/RichEditor/index.vue')['default'] + RichText: typeof import('./src/components/Charts/richText.vue')['default'] + RightAddBtn: typeof import('./src/components/Btns/RightAddBtn.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + ScatterChart: typeof import('./src/components/Charts/scatterChart.vue')['default'] + ScriptEditor: typeof import('./src/components/ScriptEditor/index.vue')['default'] + Sidebar: typeof import('./src/components/layout/components/Sidebar.vue')['default'] + StepBar: typeof import('./src/components/StepBar/index.vue')['default'] + StepItem: typeof import('./src/components/StepBar/stepItem.vue')['default'] + SubMenu: typeof import('./src/components/layout/components/SubMenu.vue')['default'] + TableBox: typeof import('./src/components/TableBox/index.vue')['default'] + TableProgressColumn: typeof import('./src/components/TableProgressColumn/index.vue')['default'] + TagItem: typeof import('./src/components/layout/components/TagItem.vue')['default'] + TagPanel: typeof import('./src/components/layout/components/TagPanel.vue')['default'] + ThirdParty: typeof import('./src/components/thirdParty/index.vue')['default'] + UserSelect: typeof import('./src/components/UserSelect/index.vue')['default'] + UserSelectDlg: typeof import('./src/components/UserSelect/UserSelectDlg.vue')['default'] + VanButton: typeof import('vant/es')['Button'] + VanCellGroup: typeof import('vant/es')['CellGroup'] + VanCheckbox: typeof import('vant/es')['Checkbox'] + VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup'] + VanForm: typeof import('vant/es')['Form'] + VanRadio: typeof import('vant/es')['Radio'] + VanRadioGroup: typeof import('vant/es')['RadioGroup'] + VanRate: typeof import('vant/es')['Rate'] + VanSearch: typeof import('vant/es')['Search'] + VanSidebar: typeof import('vant/es')['Sidebar'] + VanSidebarItem: typeof import('vant/es')['SidebarItem'] + VanStepper: typeof import('vant/es')['Stepper'] + VanSwitch: typeof import('vant/es')['Switch'] + VanUploader: typeof import('vant/es')['Uploader'] + } +} diff --git a/OrangeFormsOpen-VUE3/index.html b/OrangeFormsOpen-VUE3/index.html new file mode 100644 index 00000000..a3dda7ba --- /dev/null +++ b/OrangeFormsOpen-VUE3/index.html @@ -0,0 +1,13 @@ + + + + + + + 加载中 + + +
+ + + diff --git a/OrangeFormsOpen-VUE3/package-lock.json b/OrangeFormsOpen-VUE3/package-lock.json new file mode 100644 index 00000000..69f8b207 --- /dev/null +++ b/OrangeFormsOpen-VUE3/package-lock.json @@ -0,0 +1,12498 @@ +{ + "name": "vite", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "vite", + "version": "0.0.0", + "dependencies": { + "@highlightjs/vue-plugin": "^2.1.0", + "@layui/layui-vue": "^2.11.5", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.12", + "ace-builds": "^1.32.2", + "axios": "^1.5.1", + "bpmn-js-token-simulation": "^0.10.0", + "clipboard": "^2.0.11", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.10", + "echarts": "^5.5.0", + "ejs": "^3.1.9", + "element-plus": "^2.7.3", + "highlight.js": "^11.9.0", + "jsencrypt": "^3.3.2", + "json-bigint": "^1.0.0", + "pinia": "^2.1.6", + "pinia-plugin-persist": "^1.0.0", + "vant": "^4.7.3", + "vue": "^3.3.8", + "vue-draggable-plus": "^0.3.1", + "vue-json-viewer": "^3.0.4", + "vue-router": "^4.2.5", + "vxe-table": "^4.5.13", + "xe-utils": "^3.5.14", + "xml-js": "^1.6.11" + }, + "devDependencies": { + "@types/ejs": "^3.1.5", + "@types/json-bigint": "^1.0.4", + "@types/node": "^18.11.17", + "@typescript-eslint/eslint-plugin": "^5.46.1", + "@typescript-eslint/parser": "^5.46.1", + "@vant/auto-import-resolver": "^1.0.2", + "@vitejs/plugin-vue": "^4.0.0", + "autoprefixer": "^10.4.16", + "bpmn-js": "^7.4.0", + "bpmn-js-properties-panel": "^0.37.2", + "eslint": "^8.30.0", + "eslint-config-prettier": "^8.5.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-vue": "^9.8.0", + "postcss": "^8.4.20", + "postcss-html": "^1.5.0", + "postcss-preset-env": "^7.8.3", + "postcss-scss": "^4.0.6", + "prettier": "2.8.1", + "sass": "^1.57.1", + "typescript": "^4.9.3", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "vite": "^4.0.0", + "vite-plugin-eslint": "^1.8.1", + "vue-eslint-parser": "^9.1.0", + "vue-tsc": "^1.0.11" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.6", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.6.tgz", + "integrity": "sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==", + "dev": true + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.0.tgz", + "integrity": "sha512-uR7NWq2VNFnDi7EYqiRz2Jv/VQIu38tu64Zy8TX2nQFQ6etJ9V/Rr2msW8BS132mum2rL645qpDrLtAJtVpuow==", + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bpmn-io/extract-process-variables": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@bpmn-io/extract-process-variables/-/extract-process-variables-0.3.0.tgz", + "integrity": "sha512-cZMPBvVUXBn7++ZaOVQQGvhrMnFVcOP218yfYBKUv0EMYjo775ust/ZmfIgWd8llT4myXA6dPz12wcYXUBR1Bg==", + "dev": true, + "dependencies": { + "min-dash": "^3.5.2" + }, + "peerDependencies": { + "camunda-bpmn-moddle": "^4.x" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", + "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.16.9.tgz", + "integrity": "sha512-kW5ccqWHVOOTGUkkJbtfoImtqu3kA1PFkivM+9QPFSHphPfPBlBalX9eDRqPK+wHCqKhU48/78T791qPgC9e9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.16.9.tgz", + "integrity": "sha512-ndIAZJUeLx4O+4AJbFQCurQW4VRUXjDsUvt1L+nP8bVELOWdmdCEOtlIweCUE6P+hU0uxYbEK2AEP0n5IVQvhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.16.9.tgz", + "integrity": "sha512-UbMcJB4EHrAVOnknQklREPgclNU2CPet2h+sCBCXmF2mfoYWopBn/CfTfeyOkb/JglOcdEADqAljFndMKnFtOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.9.tgz", + "integrity": "sha512-d7D7/nrt4CxPul98lx4PXhyNZwTYtbdaHhOSdXlZuu5zZIznjqtMqLac8Bv+IuT6SVHiHUwrkL6ywD7mOgLW+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.16.9.tgz", + "integrity": "sha512-LZc+Wlz06AkJYtwWsBM3x2rSqTG8lntDuftsUNQ3fCx9ZttYtvlDcVtgb+NQ6t9s6K5No5zutN3pcjZEC2a4iQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.9.tgz", + "integrity": "sha512-gIj0UQZlQo93CHYouHKkpzP7AuruSaMIm1etcWIxccFEVqCN1xDr6BWlN9bM+ol/f0W9w3hx3HDuEwcJVtGneQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.9.tgz", + "integrity": "sha512-GNors4vaMJ7lzGOuhzNc7jvgsQZqErGA8rsW+nck8N1nYu86CvsJW2seigVrQQWOV4QzEP8Zf3gm+QCjA2hnBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.16.9.tgz", + "integrity": "sha512-cNx1EF99c2t1Ztn0lk9N+MuwBijGF8mH6nx9GFsB3e0lpUpPkCE/yt5d+7NP9EwJf5uzqdjutgVYoH1SNqzudA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.16.9.tgz", + "integrity": "sha512-YPxQunReYp8RQ1FvexFrOEqqf+nLbS3bKVZF5FRT2uKM7Wio7BeATqAwO02AyrdSEntt3I5fhFsujUChIa8CZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.16.9.tgz", + "integrity": "sha512-zb12ixDIKNwFpIqR00J88FFitVwOEwO78EiUi8wi8FXlmSc3GtUuKV/BSO+730Kglt0B47+ZrJN1BhhOxZaVrw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.16.9.tgz", + "integrity": "sha512-X8te4NLxtHiNT6H+4Pfm5RklzItA1Qy4nfyttihGGX+Koc53Ar20ViC+myY70QJ8PDEOehinXZj/F7QK3A+MKQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.9.tgz", + "integrity": "sha512-ZqyMDLt02c5smoS3enlF54ndK5zK4IpClLTxF0hHfzHJlfm4y8IAkIF8LUW0W7zxcKy7oAwI7BRDqeVvC120SA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.9.tgz", + "integrity": "sha512-k+ca5W5LDBEF3lfDwMV6YNXwm4wEpw9krMnNvvlNz3MrKSD2Eb2c861O0MaKrZkG/buTQAP4vkavbLwgIe6xjg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.9.tgz", + "integrity": "sha512-GuInVdogjmg9DhgkEmNipHkC+3tzkanPJzgzTC2ihsvrruLyFoR1YrTGixblNSMPudQLpiqkcwGwwe0oqfrvfA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.16.9.tgz", + "integrity": "sha512-49wQ0aYkvwXonGsxc7LuuLNICMX8XtO92Iqmug5Qau0kpnV6SP34jk+jIeu4suHwAbSbRhVFtDv75yRmyfQcHw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.16.9.tgz", + "integrity": "sha512-Nx4oKEAJ6EcQlt4dK7qJyuZUoXZG7CAeY22R7rqZijFzwFfMOD+gLP56uV7RrV86jGf8PeRY8TBsRmOcZoG42w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.9.tgz", + "integrity": "sha512-d0WnpgJ+FTiMZXEQ1NOv9+0gvEhttbgKEvVqWWAtl1u9AvlspKXbodKHzQ5MLP6YV1y52Xp+p8FMYqj8ykTahg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.9.tgz", + "integrity": "sha512-jccK11278dvEscHFfMk5EIPjF4wv1qGD0vps7mBV1a6TspdR36O28fgPem/SA/0pcsCPHjww5ouCLwP+JNAFlw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.16.9.tgz", + "integrity": "sha512-OetwTSsv6mIDLqN7I7I2oX9MmHGwG+AP+wKIHvq+6sIHwcPPJqRx+DJB55jy9JG13CWcdcQno/7V5MTJ5a0xfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.16.9.tgz", + "integrity": "sha512-tKSSSK6unhxbGbHg+Cc+JhRzemkcsX0tPBvG0m5qsWbkShDK9c+/LSb13L18LWVdOQZwuA55Vbakxmt6OjBDOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.16.9.tgz", + "integrity": "sha512-ZTQ5vhNS5gli0KK8I6/s6+LwXmNEfq1ftjnSVyyNm33dBw8zDpstqhGXYUbZSWWLvkqiRRjgxgmoncmi6Yy7Ng==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.16.9.tgz", + "integrity": "sha512-C4ZX+YFIp6+lPrru3tpH6Gaapy8IBRHw/e7l63fzGDhn/EaiGpQgbIlT5paByyy+oMvRFQoxxyvC4LE0AjJMqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-1.4.0.tgz", + "integrity": "sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, + "node_modules/@highlightjs/vue-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@highlightjs/vue-plugin/-/vue-plugin-2.1.0.tgz", + "integrity": "sha512-E+bmk4ncca+hBEYRV2a+1aIzIV0VSY/e5ArjpuSN9IO7wBJrzUE2u4ESCwrbQD7sAy+jWQjkV5qCCWgc+pu7CQ==", + "peerDependencies": { + "highlight.js": "^11.0.1", + "vue": "^3" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@intlify/core-base": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.10.tgz", + "integrity": "sha512-So9CNUavB/IsZ+zBmk2Cv6McQp6vc2wbGi1S0XQmJ8Vz+UFcNn9MFXAe9gY67PreIHrbLsLxDD0cwo1qsxM1Nw==", + "dependencies": { + "@intlify/devtools-if": "9.1.10", + "@intlify/message-compiler": "9.1.10", + "@intlify/message-resolver": "9.1.10", + "@intlify/runtime": "9.1.10", + "@intlify/shared": "9.1.10", + "@intlify/vue-devtools": "9.1.10" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/devtools-if": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.10.tgz", + "integrity": "sha512-SHaKoYu6sog3+Q8js1y3oXLywuogbH1sKuc7NSYkN3GElvXSBaMoCzW+we0ZSFqj/6c7vTNLg9nQ6rxhKqYwnQ==", + "dependencies": { + "@intlify/shared": "9.1.10" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.10.tgz", + "integrity": "sha512-+JiJpXff/XTb0EadYwdxOyRTB0hXNd4n1HaJ/a4yuV960uRmPXaklJsedW0LNdcptd/hYUZtCkI7Lc9J5C1gxg==", + "dependencies": { + "@intlify/message-resolver": "9.1.10", + "@intlify/shared": "9.1.10", + "source-map": "0.6.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/message-resolver": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.10.tgz", + "integrity": "sha512-5YixMG/M05m0cn9+gOzd4EZQTFRUu8RGhzxJbR1DWN21x/Z3bJ8QpDYj6hC4FwBj5uKsRfKpJQ3Xqg98KWoA+w==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/runtime": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.10.tgz", + "integrity": "sha512-7QsuByNzpe3Gfmhwq6hzgXcMPpxz8Zxb/XFI6s9lQdPLPe5Lgw4U1ovRPZTOs6Y2hwitR3j/HD8BJNGWpJnOFA==", + "dependencies": { + "@intlify/message-compiler": "9.1.10", + "@intlify/message-resolver": "9.1.10", + "@intlify/shared": "9.1.10" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/shared": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.10.tgz", + "integrity": "sha512-Om54xJeo1Vw+K1+wHYyXngE8cAbrxZHpWjYzMR9wCkqbhGtRV5VLhVc214Ze2YatPrWlS2WSMOWXR8JktX/IgA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/vue-devtools": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.10.tgz", + "integrity": "sha512-5l3qYARVbkWAkagLu1XbDUWRJSL8br1Dj60wgMaKB0+HswVsrR6LloYZTg7ozyvM621V6+zsmwzbQxbVQyrytQ==", + "dependencies": { + "@intlify/message-resolver": "9.1.10", + "@intlify/runtime": "9.1.10", + "@intlify/shared": "9.1.10" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@layui/icons-vue": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@layui/icons-vue/-/icons-vue-1.1.0.tgz", + "integrity": "sha512-ndc53qyUZSslUkO8ZHeBMh6i4gSTtAUqsPpKQZWML0JH6E/X3LIySe6LATeqEMmD7wWSnHJ+WBVGO4ij85Dk1g==" + }, + "node_modules/@layui/layer-vue": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@layui/layer-vue/-/layer-vue-2.1.1.tgz", + "integrity": "sha512-lk9UoDQmLvtqrgdK+zeizp8KZy8pQfzX7dzHhAv+Qc74L1WC2jipb2hpYmaksiKX1lihy0D9eWWycMbnRn7V9A==", + "dependencies": { + "@layui/icons-vue": "1.1.0" + } + }, + "node_modules/@layui/layui-vue": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@layui/layui-vue/-/layui-vue-2.11.5.tgz", + "integrity": "sha512-KZ5xrOm+B27yrEMWSuIGPLgLxUjISWuq0ecU4BcwrasCjEklfLS9UZBQp3peRWRsD6PGXP/cet1qQiD0AnUCJg==", + "dependencies": { + "@babel/types": "7.21.0", + "@ctrl/tinycolor": "^3.4.1", + "@layui/icons-vue": "1.1.0", + "@layui/layer-vue": "2.1.1", + "@rollup/plugin-terser": "0.4.3", + "@types/qrcode": "1.5.0", + "@umijs/ssr-darkreader": "^4.9.45", + "@vueuse/core": "8.7.3", + "async-validator": "^4.1.1", + "cropperjs": "^1.5.12", + "dayjs": "^1.11.7", + "evtd": "^0.2.3", + "jsbarcode": "3.11.5", + "qrcode": "1.5.0", + "vue-i18n": "9.1.10" + } + }, + "node_modules/@layui/layui-vue/node_modules/@vueuse/core": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.7.3.tgz", + "integrity": "sha512-jpBnyG9b4wXgk0Dz3I71lfhD0o53t1tZR+NoAQ+17zJy7MP/VDfGIkq8GcqpDwmptLCmGiGVipkPbWmDGMic8Q==", + "dependencies": { + "@vueuse/metadata": "8.7.3", + "@vueuse/shared": "8.7.3", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@layui/layui-vue/node_modules/@vueuse/metadata": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.7.3.tgz", + "integrity": "sha512-spf9kgCsBEFbQb90I6SIqAWh1yP5T1JoJGj+/04+VTMIHXKzn3iecmHUalg8QEOCPNtnFQGNEw5OLg0L39eizg==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@layui/layui-vue/node_modules/@vueuse/shared": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.7.3.tgz", + "integrity": "sha512-PMc/h6cEakJ4+5VuNUGi7RnbA6CkLvtG2230x8w3zYJpW1P6Qphh9+dFFvHn7TX+RlaicF5ND0RX1NxWmAoW7w==", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@layui/layui-vue/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==" + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", + "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.x || ^3.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz", + "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==" + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "8.4.10", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.4.10.tgz", + "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "node_modules/@types/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==" + }, + "node_modules/@types/json-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz", + "integrity": "sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.201", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.201.tgz", + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.11", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "18.11.17", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.11.17.tgz", + "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==" + }, + "node_modules/@types/qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, + "node_modules/@types/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==", + "peer": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz", + "integrity": "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.46.1", + "@typescript-eslint/type-utils": "5.46.1", + "@typescript-eslint/utils": "5.46.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-5.46.1.tgz", + "integrity": "sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.46.1", + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/typescript-estree": "5.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz", + "integrity": "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/visitor-keys": "5.46.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz", + "integrity": "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.46.1", + "@typescript-eslint/utils": "5.46.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-5.46.1.tgz", + "integrity": "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz", + "integrity": "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/visitor-keys": "5.46.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-5.46.1.tgz", + "integrity": "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.46.1", + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/typescript-estree": "5.46.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz", + "integrity": "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.46.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@umijs/ssr-darkreader": { + "version": "4.9.45", + "resolved": "https://registry.npmjs.org/@umijs/ssr-darkreader/-/ssr-darkreader-4.9.45.tgz", + "integrity": "sha512-XlcwzSYQ/SRZpHdwIyMDS4FOGX5kP4U/2g2mykyn/iPQTK4xTiQAyBu6UnnDnn7d5P8s7Atzh1C7H0ETNOypJg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/darkreader" + } + }, + "node_modules/@uppy/companion-client": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz", + "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==", + "dependencies": { + "@uppy/utils": "^4.1.2", + "namespace-emitter": "^2.0.1" + } + }, + "node_modules/@uppy/core": { + "version": "2.3.4", + "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz", + "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/store-default": "^2.1.1", + "@uppy/utils": "^4.1.3", + "lodash.throttle": "^4.1.1", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^3.1.25", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/store-default": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz", + "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==" + }, + "node_modules/@uppy/utils": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==", + "dependencies": { + "lodash.throttle": "^4.1.1" + } + }, + "node_modules/@uppy/xhr-upload": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz", + "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2", + "nanoid": "^3.1.25" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@vant/auto-import-resolver": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@vant/auto-import-resolver/-/auto-import-resolver-1.0.2.tgz", + "integrity": "sha512-5SYC1izl36KID+3F4pqFtYD8VFK6m1pdulft99sjSkUN4GBX9OslRnsJA0g7xS+0YrytjDuxxBk04YLYIxaYMg==", + "dev": true + }, + "node_modules/@vant/popperjs": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz", + "integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==" + }, + "node_modules/@vant/use": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.6.0.tgz", + "integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz", + "integrity": "sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.0.14.tgz", + "integrity": "sha512-j1tMQgw0qCV2amM4qDJNG/zc0yj3ay8HoWNt05IaiCPsULtSSpF/9+F6Izvn0DF7nWOd6MUHTxaQAeZwLfr56Q==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.0.14", + "@vue/reactivity": "^3.2.45", + "muggle-string": "^0.1.0" + } + }, + "node_modules/@volar/source-map": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.0.14.tgz", + "integrity": "sha512-8pHCbEWHWaSDGb/FM9zRIW1lY1OAo16MENVSQGCgTwz7PWf3Gw6WW3TFVKCtzaFhLjPH0i5e9hALy7vBPbSHoA==", + "dev": true, + "dependencies": { + "muggle-string": "^0.1.0" + } + }, + "node_modules/@volar/typescript": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.0.14.tgz", + "integrity": "sha512-67qcjjz7KGFhMCG9EKMA9qJK3BRGQecO4dGyAKfMfClZ/PaVoKfDvJvYo89McGTQ8SeczD48I9TPnaJM0zK8JQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.0.14" + } + }, + "node_modules/@volar/vue-language-core": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/vue-language-core/-/vue-language-core-1.0.14.tgz", + "integrity": "sha512-grJ4dQ7c/suZmBBmZtw2O2XeDX+rtgpdBtHxMug1NMPRDxj5EZ9WGphWtGnMQj8RyVgpz9ByvV5GbQjk4/wfBw==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.0.14", + "@volar/source-map": "1.0.14", + "@vue/compiler-dom": "^3.2.45", + "@vue/compiler-sfc": "^3.2.45", + "@vue/reactivity": "^3.2.45", + "@vue/shared": "^3.2.45", + "minimatch": "^5.1.0", + "vue-template-compiler": "^2.7.14" + } + }, + "node_modules/@volar/vue-typescript": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/vue-typescript/-/vue-typescript-1.0.14.tgz", + "integrity": "sha512-2P0QeGLLY05fDTu8GqY8SR2+jldXRTrkQdD2Nc0sVOjMJ7j3RYYY0wJyZ9hCBDuxV4Micc6jdB8nKS0yxQgNvA==", + "deprecated": "WARNING: This project has been renamed to @vue/typescript. Install using @vue/typescript instead.", + "dev": true, + "dependencies": { + "@volar/typescript": "1.0.14", + "@volar/vue-language-core": "1.0.14" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz", + "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz", + "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==", + "dependencies": { + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz", + "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-ssr": "3.3.8", + "@vue/reactivity-transform": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz", + "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==", + "dependencies": { + "@vue/compiler-dom": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "node_modules/@vue/reactivity": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz", + "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==", + "dependencies": { + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz", + "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz", + "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==", + "dependencies": { + "@vue/reactivity": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz", + "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==", + "dependencies": { + "@vue/runtime-core": "3.3.8", + "@vue/shared": "3.3.8", + "csstype": "^3.1.2" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz", + "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==", + "dependencies": { + "@vue/compiler-ssr": "3.3.8", + "@vue/shared": "3.3.8" + }, + "peerDependencies": { + "vue": "3.3.8" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz", + "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==" + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "dependencies": { + "vue-demi": "*" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@wangeditor/basic-modules": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz", + "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==", + "dependencies": { + "is-url": "^1.2.4" + }, + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/code-highlight": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz", + "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==", + "dependencies": { + "prismjs": "^1.23.0" + }, + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/core": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz", + "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==", + "dependencies": { + "@types/event-emitter": "^0.3.3", + "event-emitter": "^0.3.5", + "html-void-elements": "^2.0.0", + "i18next": "^20.4.0", + "scroll-into-view-if-needed": "^2.2.28", + "slate-history": "^0.66.0" + }, + "peerDependencies": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/editor": { + "version": "5.1.23", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz", + "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==", + "dependencies": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "^1.1.7", + "@wangeditor/code-highlight": "^1.0.3", + "@wangeditor/core": "^1.1.19", + "@wangeditor/list-module": "^1.0.5", + "@wangeditor/table-module": "^1.1.4", + "@wangeditor/upload-image-module": "^1.0.2", + "@wangeditor/video-module": "^1.1.4", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/editor-for-vue": { + "version": "5.1.12", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz", + "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==", + "peerDependencies": { + "@wangeditor/editor": ">=5.1.0", + "vue": "^3.0.5" + } + }, + "node_modules/@wangeditor/list-module": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz", + "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==", + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/table-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz", + "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==", + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/upload-image-module": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz", + "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==", + "peerDependencies": { + "@uppy/core": "^2.0.3", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "1.x", + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.foreach": "^4.5.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/video-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz", + "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==", + "peerDependencies": { + "@uppy/core": "^2.1.4", + "@uppy/xhr-upload": "^2.0.7", + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/ace-builds": { + "version": "1.32.2", + "resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.32.2.tgz", + "integrity": "sha512-mnJAc803p+7eeDt07r6XI7ufV7VdkpPq4gJZT8Jb3QsowkaBTVy4tdBgPrVT0WbXLm0toyEQXURKSVNj/7dfJQ==" + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bpmn-js": { + "version": "7.5.0", + "resolved": "https://registry.npmmirror.com/bpmn-js/-/bpmn-js-7.5.0.tgz", + "integrity": "sha512-0ANaE6Bikg1GmkcvO7RK0MQPX+EKYKBc+q7OWk39/16NcCdNZ/4UiRcCr9n0u1VUCIDsSU/jJ79TIZFnV5CNjw==", + "dev": true, + "dependencies": { + "bpmn-moddle": "^7.0.4", + "css.escape": "^1.5.1", + "diagram-js": "^6.8.2", + "diagram-js-direct-editing": "^1.6.1", + "ids": "^1.0.0", + "inherits": "^2.0.4", + "min-dash": "^3.5.2", + "min-dom": "^3.1.3", + "object-refs": "^0.3.0", + "tiny-svg": "^2.2.2" + } + }, + "node_modules/bpmn-js-properties-panel": { + "version": "0.37.6", + "resolved": "https://registry.npmmirror.com/bpmn-js-properties-panel/-/bpmn-js-properties-panel-0.37.6.tgz", + "integrity": "sha512-1rP9r6ItL1gKqXezXnpr9eVsQtdufH6TNqxUs11Q68CtxeBAs0l1wEHw2f01i9ceHHxItmrZUTndqnASi89EYA==", + "dev": true, + "dependencies": { + "@bpmn-io/extract-process-variables": "^0.3.0", + "ids": "^1.0.0", + "inherits": "^2.0.1", + "lodash": "^4.17.20", + "min-dom": "^3.1.3", + "scroll-tabs": "^1.0.1", + "selection-update": "^0.1.2" + }, + "peerDependencies": { + "bpmn-js": "^3.x || ^4.x || ^5.x || ^6.x || ^7.x" + } + }, + "node_modules/bpmn-js-token-simulation": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.10.0.tgz", + "integrity": "sha512-QuZQ/KVXKt9Vl+XENyOBoTW2Aw+uKjuBlKdCJL6El7AyM7DkJ5bZkSYURshId1SkBDdYg2mJ1flSmsrhGuSfwg==", + "dependencies": { + "min-dash": "^3.3.0", + "min-dom": "^0.2.0", + "svg.js": "^2.6.3" + } + }, + "node_modules/bpmn-js-token-simulation/node_modules/min-dom": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-0.2.0.tgz", + "integrity": "sha512-VmxugbnAcVZGqvepjhOA4d4apmrpX8mMaRS+/jo0dI5Yorzrr4Ru9zc9KVALlY/+XakVCb8iQ+PYXljihQcsNw==", + "dependencies": { + "component-classes": "^1.2.3", + "component-closest": "^0.1.4", + "component-delegate": "^0.2.3", + "component-event": "^0.1.4", + "component-matches-selector": "^0.1.5", + "component-query": "^0.0.3", + "domify": "^1.3.1" + } + }, + "node_modules/bpmn-moddle": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-7.1.3.tgz", + "integrity": "sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==", + "dev": true, + "dependencies": { + "min-dash": "^3.5.2", + "moddle": "^5.0.2", + "moddle-xml": "^9.0.6" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camunda-bpmn-moddle": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/camunda-bpmn-moddle/-/camunda-bpmn-moddle-4.5.0.tgz", + "integrity": "sha512-g3d2ZaCac52WIXP3kwmYrBEkhm0nnXcWYNj5STDkmiWpDTKUzTj4ZIt38IRpci1Uj3a/rZACvXLnQj8xKFyp/w==", + "dev": true, + "peer": true, + "dependencies": { + "min-dash": "^3.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001550", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001550.tgz", + "integrity": "sha512-p82WjBYIypO0ukTsd/FG3Xxs+4tFeaY9pfT4amQL8KWtYH7H9nYwReGAbMTJ0hsmRO8IfDtsS6p3ZWj8+1c2RQ==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/component-classes": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/component-classes/-/component-classes-1.2.6.tgz", + "integrity": "sha512-hPFGULxdwugu1QWW3SvVOCUHLzO34+a2J6Wqy0c5ASQkfi9/8nZcBB0ZohaEbXOQlCflMAEMmEWk7u7BVs4koA==", + "dependencies": { + "component-indexof": "0.0.3" + } + }, + "node_modules/component-closest": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/component-closest/-/component-closest-0.1.4.tgz", + "integrity": "sha512-NF9hMj6JKGM5sb6wP/dg7GdJOttaIH9PcTsUNdWcrvu7Kw/5R5swQAFpgaYEHlARrNMyn4Wf7O1PlRej+pt76Q==", + "dependencies": { + "component-matches-selector": "~0.1.5" + } + }, + "node_modules/component-delegate": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/component-delegate/-/component-delegate-0.2.4.tgz", + "integrity": "sha512-OlpcB/6Fi+kXQPh/TfXnSvvmrU04ghz7vcJh/jgLF0Ni+I+E3WGlKJQbBGDa5X+kVUG8WxOgjP+8iWbz902fPg==", + "dependencies": { + "component-closest": "*", + "component-event": "*" + } + }, + "node_modules/component-event": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/component-event/-/component-event-0.1.4.tgz", + "integrity": "sha512-GMwOG8MnUHP1l8DZx1ztFO0SJTFnIzZnBDkXAj8RM2ntV2A6ALlDxgbMY1Fvxlg6WPQ+5IM/a6vg4PEYbjg/Rw==" + }, + "node_modules/component-indexof": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/component-indexof/-/component-indexof-0.0.3.tgz", + "integrity": "sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==" + }, + "node_modules/component-matches-selector": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/component-matches-selector/-/component-matches-selector-0.1.7.tgz", + "integrity": "sha512-Yb2+pVBvrqkQVpPaDBF0DYXRreBveXJNrpJs9FnFu8PF6/5IIcz5oDZqiH9nB5hbD2/TmFVN5ZCxBzqu7yFFYQ==", + "dependencies": { + "component-query": "*", + "global-object": "^1.0.0" + } + }, + "node_modules/component-query": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/component-query/-/component-query-0.0.3.tgz", + "integrity": "sha512-VgebQseT1hz1Ps7vVp2uaSg+N/gsI5ts3AZUSnN6GMA2M82JH7o+qYifWhmVE/e8w/H48SJuA3nA9uX8zRe95Q==" + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cropperjs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.1.tgz", + "integrity": "sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssdb": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/cssdb/-/cssdb-7.2.0.tgz", + "integrity": "sha512-JYlIsE7eKHSi0UNuCyo96YuIDFqvhGgHw4Ck6lsN+DP0Tp8M64UTDT2trGbkMDqnCoEjks7CkS0XcjU0rkvBdg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "node_modules/diagram-js": { + "version": "6.8.2", + "resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-6.8.2.tgz", + "integrity": "sha512-5EKYHjW2mmGsn9/jSenSkm8cScK5sO9eETBRQNIIzgZjxBDJn6eX964L2d7/vrAW9SeuijGUsztL9+NUinSsNg==", + "dev": true, + "dependencies": { + "css.escape": "^1.5.1", + "didi": "^4.0.0", + "hammerjs": "^2.0.1", + "inherits": "^2.0.1", + "min-dash": "^3.5.0", + "min-dom": "^3.1.2", + "object-refs": "^0.3.0", + "path-intersection": "^2.2.0", + "tiny-svg": "^2.2.1" + } + }, + "node_modules/diagram-js-direct-editing": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-1.8.0.tgz", + "integrity": "sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==", + "dev": true, + "dependencies": { + "min-dash": "^3.5.2", + "min-dom": "^3.1.3" + }, + "peerDependencies": { + "diagram-js": "*" + } + }, + "node_modules/didi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/didi/-/didi-4.0.0.tgz", + "integrity": "sha512-AzMElh8mCHOPWPCWfGjoJRla31fMXUT6+287W5ef3IPmtuBcyG9+MkFS7uPP6v3t2Cl086KwWfRB9mESa0OsHQ==", + "dev": true + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "node_modules/dom-zindex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dom-zindex/-/dom-zindex-1.0.1.tgz", + "integrity": "sha512-M/MERVDZ8hguvjl6MAlLWSLYLS7PzEyXaTb5gEeJ+SF+e9iUC0sdvlzqe91MMDHBoy+nqw7wKcUOrDSyvMCrRg==" + }, + "node_modules/dom7": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz", + "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==", + "dependencies": { + "ssr-window": "^3.0.0-alpha.1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/domify": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/domify/-/domify-1.4.2.tgz", + "integrity": "sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==" + }, + "node_modules/domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, + "node_modules/echarts": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.5.0.tgz", + "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.5.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.559", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.559.tgz", + "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==", + "dev": true + }, + "node_modules/element-plus": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz", + "integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.3", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/esbuild": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.16.9.tgz", + "integrity": "sha512-gkH83yHyijMSZcZFs1IWew342eMdFuWXmQo3zkDPTre25LIPBJsXryg02M3u8OpTwCJdBkdaQwqKkDLnAsAeLQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.16.9", + "@esbuild/android-arm64": "0.16.9", + "@esbuild/android-x64": "0.16.9", + "@esbuild/darwin-arm64": "0.16.9", + "@esbuild/darwin-x64": "0.16.9", + "@esbuild/freebsd-arm64": "0.16.9", + "@esbuild/freebsd-x64": "0.16.9", + "@esbuild/linux-arm": "0.16.9", + "@esbuild/linux-arm64": "0.16.9", + "@esbuild/linux-ia32": "0.16.9", + "@esbuild/linux-loong64": "0.16.9", + "@esbuild/linux-mips64el": "0.16.9", + "@esbuild/linux-ppc64": "0.16.9", + "@esbuild/linux-riscv64": "0.16.9", + "@esbuild/linux-s390x": "0.16.9", + "@esbuild/linux-x64": "0.16.9", + "@esbuild/netbsd-x64": "0.16.9", + "@esbuild/openbsd-x64": "0.16.9", + "@esbuild/sunos-x64": "0.16.9", + "@esbuild/win32-arm64": "0.16.9", + "@esbuild/win32-ia32": "0.16.9", + "@esbuild/win32-x64": "0.16.9" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint": { + "version": "8.30.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.30.0.tgz", + "integrity": "sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.4.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.8.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.8.0.tgz", + "integrity": "sha512-E/AXwcTzunyzM83C2QqDHxepMzvI2y6x+mmeYHbVDQlKFqmKYvRrhaVixEeeG27uI44p9oKDFiyCRw4XxgtfHA==", + "dev": true, + "dependencies": { + "eslint-utils": "^3.0.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", + "postcss-selector-parser": "^6.0.9", + "semver": "^7.3.5", + "vue-eslint-parser": "^9.0.1", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==" + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-object": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/global-object/-/global-object-1.0.0.tgz", + "integrity": "sha512-mSPSkY6UsHv6hgW0V2dfWBWTS8TnPnLx3ECVNoWp6rBI2Bg66VYoqGoTFlH/l7XhAZ/l+StYlntXlt87BEeCcg==" + }, + "node_modules/globals": { + "version": "13.19.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" + }, + "node_modules/htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, + "node_modules/i18next": { + "version": "20.6.1", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz", + "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, + "node_modules/ids": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/ids/-/ids-1.0.5.tgz", + "integrity": "sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.2.tgz", + "integrity": "sha512-m1MJSy4Z2NAcyhoYpxQeBsc1ZdNQwYjN0wGbLBlnVArdJ90Gtr8IhNSfZZcCoR0fM/0E0BJ0mf1KnLNDOCJP4w==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbarcode": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz", + "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==", + "bin": { + "auto.js": "bin/barcodes/CODE128/auto.js", + "Barcode.js": "bin/barcodes/Barcode.js", + "barcodes": "bin/barcodes", + "canvas.js": "bin/renderers/canvas.js", + "checksums.js": "bin/barcodes/MSI/checksums.js", + "codabar": "bin/barcodes/codabar", + "CODE128": "bin/barcodes/CODE128", + "CODE128_AUTO.js": "bin/barcodes/CODE128/CODE128_AUTO.js", + "CODE128.js": "bin/barcodes/CODE128/CODE128.js", + "CODE128A.js": "bin/barcodes/CODE128/CODE128A.js", + "CODE128B.js": "bin/barcodes/CODE128/CODE128B.js", + "CODE128C.js": "bin/barcodes/CODE128/CODE128C.js", + "CODE39": "bin/barcodes/CODE39", + "constants.js": "bin/barcodes/ITF/constants.js", + "defaults.js": "bin/options/defaults.js", + "EAN_UPC": "bin/barcodes/EAN_UPC", + "EAN.js": "bin/barcodes/EAN_UPC/EAN.js", + "EAN13.js": "bin/barcodes/EAN_UPC/EAN13.js", + "EAN2.js": "bin/barcodes/EAN_UPC/EAN2.js", + "EAN5.js": "bin/barcodes/EAN_UPC/EAN5.js", + "EAN8.js": "bin/barcodes/EAN_UPC/EAN8.js", + "encoder.js": "bin/barcodes/EAN_UPC/encoder.js", + "ErrorHandler.js": "bin/exceptions/ErrorHandler.js", + "exceptions": "bin/exceptions", + "exceptions.js": "bin/exceptions/exceptions.js", + "fixOptions.js": "bin/help/fixOptions.js", + "GenericBarcode": "bin/barcodes/GenericBarcode", + "getOptionsFromElement.js": "bin/help/getOptionsFromElement.js", + "getRenderProperties.js": "bin/help/getRenderProperties.js", + "help": "bin/help", + "index.js": "bin/renderers/index.js", + "index.tmp.js": "bin/barcodes/index.tmp.js", + "ITF": "bin/barcodes/ITF", + "ITF.js": "bin/barcodes/ITF/ITF.js", + "ITF14.js": "bin/barcodes/ITF/ITF14.js", + "JsBarcode.js": "bin/JsBarcode.js", + "linearizeEncodings.js": "bin/help/linearizeEncodings.js", + "merge.js": "bin/help/merge.js", + "MSI": "bin/barcodes/MSI", + "MSI.js": "bin/barcodes/MSI/MSI.js", + "MSI10.js": "bin/barcodes/MSI/MSI10.js", + "MSI1010.js": "bin/barcodes/MSI/MSI1010.js", + "MSI11.js": "bin/barcodes/MSI/MSI11.js", + "MSI1110.js": "bin/barcodes/MSI/MSI1110.js", + "object.js": "bin/renderers/object.js", + "options": "bin/options", + "optionsFromStrings.js": "bin/help/optionsFromStrings.js", + "pharmacode": "bin/barcodes/pharmacode", + "renderers": "bin/renderers", + "shared.js": "bin/renderers/shared.js", + "svg.js": "bin/renderers/svg.js", + "UPC.js": "bin/barcodes/EAN_UPC/UPC.js", + "UPCE.js": "bin/barcodes/EAN_UPC/UPCE.js" + } + }, + "node_modules/jsencrypt": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.3.2.tgz", + "integrity": "sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/matches-selector": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/matches-selector/-/matches-selector-1.2.0.tgz", + "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==", + "dev": true + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "dependencies": { + "wildcard": "^1.1.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-dash": { + "version": "3.8.1", + "resolved": "https://registry.npmmirror.com/min-dash/-/min-dash-3.8.1.tgz", + "integrity": "sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==" + }, + "node_modules/min-dom": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-3.2.1.tgz", + "integrity": "sha512-v6YCmnDzxk4rRJntWTUiwggLupPw/8ZSRqUq0PDaBwVZEO/wYzCH4SKVBV+KkEvf3u0XaWHly5JEosPtqRATZA==", + "dev": true, + "dependencies": { + "component-event": "^0.1.4", + "domify": "^1.3.1", + "indexof": "0.0.1", + "matches-selector": "^1.2.0", + "min-dash": "^3.8.1" + } + }, + "node_modules/minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true + }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "node_modules/moddle": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/moddle/-/moddle-5.0.4.tgz", + "integrity": "sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==", + "dev": true, + "dependencies": { + "min-dash": "^3.0.0" + } + }, + "node_modules/moddle-xml": { + "version": "9.0.6", + "resolved": "https://registry.npmmirror.com/moddle-xml/-/moddle-xml-9.0.6.tgz", + "integrity": "sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==", + "dev": true, + "dependencies": { + "min-dash": "^3.5.2", + "moddle": "^5.0.2", + "saxen": "^8.1.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/muggle-string": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.1.0.tgz", + "integrity": "sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==", + "dev": true + }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==" + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-refs": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/object-refs/-/object-refs-0.3.0.tgz", + "integrity": "sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==", + "dev": true + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-intersection": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/path-intersection/-/path-intersection-2.2.1.tgz", + "integrity": "sha512-9u8xvMcSfuOiStv9bPdnRJQhGQXLKurew94n4GPQCdH1nj9QKC9ObbNoIpiRq8skiOBxKkt277PgOoFgAt3/rA==", + "dev": true + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/pinia": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz", + "integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persist": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/pinia-plugin-persist/-/pinia-plugin-persist-1.0.0.tgz", + "integrity": "sha512-M4hBBd8fz/GgNmUPaaUsC29y1M09lqbXrMAHcusVoU8xlQi1TqgkWnnhvMikZwr7Le/hVyMx8KUcumGGrR6GVw==", + "dependencies": { + "vue-demi": "^0.12.1" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0", + "pinia": "^2.0.0", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persist/node_modules/vue-demi": { + "version": "0.12.5", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.12.5.tgz", + "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmmirror.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmmirror.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmmirror.com/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-html": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/postcss-html/-/postcss-html-1.5.0.tgz", + "integrity": "sha512-kCMRWJRHKicpA166kc2lAVUGxDZL324bkj/pVOb6RhjB0Z5Krl7mN0AsVkBhVIRZZirY0lyQXG38HCVaoKVNoA==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0", + "js-tokens": "^8.0.0", + "postcss": "^8.4.0", + "postcss-safe-parser": "^6.0.0" + }, + "engines": { + "node": "^12 || >=14" + } + }, + "node_modules/postcss-html/node_modules/js-tokens": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-8.0.0.tgz", + "integrity": "sha512-PC7MzqInq9OqKyTXfIvQNcjMkODJYC8A17kAaQgeW79yfhqTWSOfjHYQ2mDDcwJ96Iibtwkfh0C7R/OvqPlgVA==", + "dev": true + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dev": true, + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmmirror.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.19" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/preact": { + "version": "10.19.3", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.19.3.tgz", + "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-2.8.1.tgz", + "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "3.7.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.7.5.tgz", + "integrity": "sha512-z0ZbqHBtS/et2EEUKMrAl2CoSdwN7ZPzL17UMiKN9RjjqHShTlv7F9J6ZJZJNREYjBh3TvBrdfjkFDIXFNeuiQ==", + "devOptional": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "node_modules/sass": { + "version": "1.57.1", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/saxen": { + "version": "8.1.2", + "resolved": "https://registry.npmmirror.com/saxen/-/saxen-8.1.2.tgz", + "integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==", + "dev": true + }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "node_modules/scroll-tabs": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/scroll-tabs/-/scroll-tabs-1.0.1.tgz", + "integrity": "sha512-W4xjEwNS4QAyQnaJ450vQTcKpbnalBAfsTDV926WrxEMOqjyj2To8uv2d0Cp0oxMdk5TkygtzXmctPNc2zgBcg==", + "dev": true, + "dependencies": { + "min-dash": "^3.1.0", + "min-dom": "^3.1.0", + "mitt": "^1.1.3" + } + }, + "node_modules/scule": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.0.0.tgz", + "integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==", + "dev": true + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" + }, + "node_modules/selection-update": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/selection-update/-/selection-update-0.1.2.tgz", + "integrity": "sha512-4jzoJNh7VT2s2tvm/kUSskSw7pD0BVcrrGccbfOMK+3AXLBPz6nIy1yo+pbXgvNoTNII96Pq92+sAY+rF0LUAA==", + "dev": true + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slate": { + "version": "0.72.8", + "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz", + "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==", + "dependencies": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-history": { + "version": "0.66.0", + "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz", + "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/smob": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==" + }, + "node_modules/snabbdom": { + "version": "3.5.1", + "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz", + "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/ssr-window": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz", + "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "node_modules/tiny-svg": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/tiny-svg/-/tiny-svg-2.2.4.tgz", + "integrity": "sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==", + "dev": true + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmmirror.com/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "node_modules/typescript": { + "version": "4.9.4", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "node_modules/unimport": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.4.0.tgz", + "integrity": "sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.4", + "escape-string-regexp": "^5.0.0", + "fast-glob": "^3.3.1", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.3", + "mlly": "^1.4.2", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "scule": "^1.0.0", + "strip-literal": "^1.3.0", + "unplugin": "^1.5.0" + } + }, + "node_modules/unimport/node_modules/@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/unplugin": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.5.0.tgz", + "integrity": "sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.7.tgz", + "integrity": "sha512-w7XmnRlchq6YUFJVFGSvG1T/6j8GrdYN6Em9Wf0Ye+HXgD/22kont+WnuCAA0UaUoxtuvRR1u/mXKy63g/hfqQ==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.5", + "fast-glob": "^3.3.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "minimatch": "^9.0.3", + "unimport": "^3.4.0", + "unplugin": "^1.5.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-auto-import/node_modules/@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/unplugin-auto-import/node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/unplugin-auto-import/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.25.2", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.2.tgz", + "integrity": "sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.5", + "@rollup/pluginutils": "^5.0.2", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.0", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "minimatch": "^9.0.3", + "resolve": "^1.22.2", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vant": { + "version": "4.7.3", + "resolved": "https://registry.npmmirror.com/vant/-/vant-4.7.3.tgz", + "integrity": "sha512-nb0pXxKSOaE9CvH//KozKDivqhjE4ZRvx1b/RCWFL4H3tZ5l+HhWtwK1yJx5AkO1Pm/IYQY86yZa1tums8DfsQ==", + "dependencies": { + "@vant/popperjs": "^1.3.0", + "@vant/use": "^1.6.0", + "@vue/shared": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/vite/-/vite-4.0.2.tgz", + "integrity": "sha512-QJaY3R+tFlTagH0exVqbgkkw45B+/bXVBzF2ZD1KB5Z8RiAoiKo60vSUf6/r4c2Vh9jfGBKM4oBI9b4/1ZJYng==", + "dev": true, + "dependencies": { + "esbuild": "^0.16.3", + "postcss": "^8.4.20", + "resolve": "^1.22.1", + "rollup": "^3.7.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-eslint": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", + "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^4.2.1", + "@types/eslint": "^8.4.5", + "rollup": "^2.77.2" + }, + "peerDependencies": { + "eslint": ">=7", + "vite": ">=2" + } + }, + "node_modules/vite-plugin-eslint/node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vue": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz", + "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==", + "dependencies": { + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-sfc": "3.3.8", + "@vue/runtime-dom": "3.3.8", + "@vue/server-renderer": "3.3.8", + "@vue/shared": "3.3.8" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-draggable-plus": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.3.1.tgz", + "integrity": "sha512-Ubo0O8/D+hZPHb1bcDTjOE42a//OjLQwj+bQwfxa1WnEKTJdS7MU0A4auUcNjyIkhTN1xuETOR4mT+BGZCPL2g==", + "peerDependencies": { + "@types/sortablejs": "^1.15.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz", + "integrity": "sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-i18n": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.10.tgz", + "integrity": "sha512-jpr7gV5KPk4n+sSPdpZT8Qx3XzTcNDWffRlHV/cT2NUyEf+sEgTTmLvnBAibjOFJ0zsUyZlVTAWH5DDnYep+1g==", + "dependencies": { + "@intlify/core-base": "9.1.10", + "@intlify/shared": "9.1.10", + "@intlify/vue-devtools": "9.1.10", + "@vue/devtools-api": "^6.0.0-beta.7" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-json-viewer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/vue-json-viewer/-/vue-json-viewer-3.0.4.tgz", + "integrity": "sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==", + "dependencies": { + "clipboard": "^2.0.4" + }, + "peerDependencies": { + "vue": "^3.2.2" + } + }, + "node_modules/vue-router": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.5.tgz", + "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==", + "dependencies": { + "@vue/devtools-api": "^6.5.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.0.14.tgz", + "integrity": "sha512-HeqtyxMrSRUCnU5nxB0lQc3o7zirMppZ/V6HLL3l4FsObGepH3A3beNmNehpLQs0Gt7DkSWVi3CpVCFgrf+/sQ==", + "dev": true, + "dependencies": { + "@volar/vue-language-core": "1.0.14", + "@volar/vue-typescript": "1.0.14" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/vxe-table": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/vxe-table/-/vxe-table-4.5.13.tgz", + "integrity": "sha512-CKsyUhDYIcO4TSXoO0I2YVkKEWjQLUq24PN6MhmFmvyFRdfj80cgLZ4iEjihLieW4aRqPcLHqkw83hCAyzvO8w==", + "dependencies": { + "dom-zindex": "^1.0.1", + "xe-utils": "^3.5.13" + }, + "peerDependencies": { + "vue": "^3.2.28", + "xe-utils": "^3.5.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==" + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xe-utils": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/xe-utils/-/xe-utils-3.5.14.tgz", + "integrity": "sha512-Xq6mS8dWwHBQsQUEBXcZYSaBV0KnNLoVWd0vRRDI3nKpbNxfs/LSCK0W21g1edLFnXYfKqg7hh5dakr3RtYY0A==" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/zrender": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.5.0.tgz", + "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + }, + "dependencies": { + "@antfu/utils": { + "version": "0.7.6", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.6.tgz", + "integrity": "sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" + }, + "@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.0.tgz", + "integrity": "sha512-uR7NWq2VNFnDi7EYqiRz2Jv/VQIu38tu64Zy8TX2nQFQ6etJ9V/Rr2msW8BS132mum2rL645qpDrLtAJtVpuow==", + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@bpmn-io/extract-process-variables": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@bpmn-io/extract-process-variables/-/extract-process-variables-0.3.0.tgz", + "integrity": "sha512-cZMPBvVUXBn7++ZaOVQQGvhrMnFVcOP218yfYBKUv0EMYjo775ust/ZmfIgWd8llT4myXA6dPz12wcYXUBR1Bg==", + "dev": true, + "requires": { + "min-dash": "^3.5.2" + } + }, + "@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "dev": true, + "requires": {} + }, + "@csstools/selector-specificity": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", + "dev": true, + "requires": {} + }, + "@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==" + }, + "@element-plus/icons-vue": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", + "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==", + "requires": {} + }, + "@esbuild/android-arm": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.16.9.tgz", + "integrity": "sha512-kW5ccqWHVOOTGUkkJbtfoImtqu3kA1PFkivM+9QPFSHphPfPBlBalX9eDRqPK+wHCqKhU48/78T791qPgC9e9A==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.16.9.tgz", + "integrity": "sha512-ndIAZJUeLx4O+4AJbFQCurQW4VRUXjDsUvt1L+nP8bVELOWdmdCEOtlIweCUE6P+hU0uxYbEK2AEP0n5IVQvhg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.16.9.tgz", + "integrity": "sha512-UbMcJB4EHrAVOnknQklREPgclNU2CPet2h+sCBCXmF2mfoYWopBn/CfTfeyOkb/JglOcdEADqAljFndMKnFtOw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.9.tgz", + "integrity": "sha512-d7D7/nrt4CxPul98lx4PXhyNZwTYtbdaHhOSdXlZuu5zZIznjqtMqLac8Bv+IuT6SVHiHUwrkL6ywD7mOgLW+A==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.16.9.tgz", + "integrity": "sha512-LZc+Wlz06AkJYtwWsBM3x2rSqTG8lntDuftsUNQ3fCx9ZttYtvlDcVtgb+NQ6t9s6K5No5zutN3pcjZEC2a4iQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.9.tgz", + "integrity": "sha512-gIj0UQZlQo93CHYouHKkpzP7AuruSaMIm1etcWIxccFEVqCN1xDr6BWlN9bM+ol/f0W9w3hx3HDuEwcJVtGneQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.9.tgz", + "integrity": "sha512-GNors4vaMJ7lzGOuhzNc7jvgsQZqErGA8rsW+nck8N1nYu86CvsJW2seigVrQQWOV4QzEP8Zf3gm+QCjA2hnBQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.16.9.tgz", + "integrity": "sha512-cNx1EF99c2t1Ztn0lk9N+MuwBijGF8mH6nx9GFsB3e0lpUpPkCE/yt5d+7NP9EwJf5uzqdjutgVYoH1SNqzudA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.16.9.tgz", + "integrity": "sha512-YPxQunReYp8RQ1FvexFrOEqqf+nLbS3bKVZF5FRT2uKM7Wio7BeATqAwO02AyrdSEntt3I5fhFsujUChIa8CZg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.16.9.tgz", + "integrity": "sha512-zb12ixDIKNwFpIqR00J88FFitVwOEwO78EiUi8wi8FXlmSc3GtUuKV/BSO+730Kglt0B47+ZrJN1BhhOxZaVrw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.16.9.tgz", + "integrity": "sha512-X8te4NLxtHiNT6H+4Pfm5RklzItA1Qy4nfyttihGGX+Koc53Ar20ViC+myY70QJ8PDEOehinXZj/F7QK3A+MKQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.9.tgz", + "integrity": "sha512-ZqyMDLt02c5smoS3enlF54ndK5zK4IpClLTxF0hHfzHJlfm4y8IAkIF8LUW0W7zxcKy7oAwI7BRDqeVvC120SA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.9.tgz", + "integrity": "sha512-k+ca5W5LDBEF3lfDwMV6YNXwm4wEpw9krMnNvvlNz3MrKSD2Eb2c861O0MaKrZkG/buTQAP4vkavbLwgIe6xjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.9.tgz", + "integrity": "sha512-GuInVdogjmg9DhgkEmNipHkC+3tzkanPJzgzTC2ihsvrruLyFoR1YrTGixblNSMPudQLpiqkcwGwwe0oqfrvfA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.16.9.tgz", + "integrity": "sha512-49wQ0aYkvwXonGsxc7LuuLNICMX8XtO92Iqmug5Qau0kpnV6SP34jk+jIeu4suHwAbSbRhVFtDv75yRmyfQcHw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.16.9.tgz", + "integrity": "sha512-Nx4oKEAJ6EcQlt4dK7qJyuZUoXZG7CAeY22R7rqZijFzwFfMOD+gLP56uV7RrV86jGf8PeRY8TBsRmOcZoG42w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.9.tgz", + "integrity": "sha512-d0WnpgJ+FTiMZXEQ1NOv9+0gvEhttbgKEvVqWWAtl1u9AvlspKXbodKHzQ5MLP6YV1y52Xp+p8FMYqj8ykTahg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.9.tgz", + "integrity": "sha512-jccK11278dvEscHFfMk5EIPjF4wv1qGD0vps7mBV1a6TspdR36O28fgPem/SA/0pcsCPHjww5ouCLwP+JNAFlw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.16.9.tgz", + "integrity": "sha512-OetwTSsv6mIDLqN7I7I2oX9MmHGwG+AP+wKIHvq+6sIHwcPPJqRx+DJB55jy9JG13CWcdcQno/7V5MTJ5a0xfQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.16.9.tgz", + "integrity": "sha512-tKSSSK6unhxbGbHg+Cc+JhRzemkcsX0tPBvG0m5qsWbkShDK9c+/LSb13L18LWVdOQZwuA55Vbakxmt6OjBDOQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.16.9.tgz", + "integrity": "sha512-ZTQ5vhNS5gli0KK8I6/s6+LwXmNEfq1ftjnSVyyNm33dBw8zDpstqhGXYUbZSWWLvkqiRRjgxgmoncmi6Yy7Ng==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.16.9.tgz", + "integrity": "sha512-C4ZX+YFIp6+lPrru3tpH6Gaapy8IBRHw/e7l63fzGDhn/EaiGpQgbIlT5paByyy+oMvRFQoxxyvC4LE0AjJMqQ==", + "dev": true, + "optional": true + }, + "@eslint/eslintrc": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-1.4.0.tgz", + "integrity": "sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "requires": { + "@floating-ui/utils": "^0.1.3" + } + }, + "@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "requires": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, + "@highlightjs/vue-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@highlightjs/vue-plugin/-/vue-plugin-2.1.0.tgz", + "integrity": "sha512-E+bmk4ncca+hBEYRV2a+1aIzIV0VSY/e5ArjpuSN9IO7wBJrzUE2u4ESCwrbQD7sAy+jWQjkV5qCCWgc+pu7CQ==", + "requires": {} + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@intlify/core-base": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.10.tgz", + "integrity": "sha512-So9CNUavB/IsZ+zBmk2Cv6McQp6vc2wbGi1S0XQmJ8Vz+UFcNn9MFXAe9gY67PreIHrbLsLxDD0cwo1qsxM1Nw==", + "requires": { + "@intlify/devtools-if": "9.1.10", + "@intlify/message-compiler": "9.1.10", + "@intlify/message-resolver": "9.1.10", + "@intlify/runtime": "9.1.10", + "@intlify/shared": "9.1.10", + "@intlify/vue-devtools": "9.1.10" + } + }, + "@intlify/devtools-if": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.10.tgz", + "integrity": "sha512-SHaKoYu6sog3+Q8js1y3oXLywuogbH1sKuc7NSYkN3GElvXSBaMoCzW+we0ZSFqj/6c7vTNLg9nQ6rxhKqYwnQ==", + "requires": { + "@intlify/shared": "9.1.10" + } + }, + "@intlify/message-compiler": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.10.tgz", + "integrity": "sha512-+JiJpXff/XTb0EadYwdxOyRTB0hXNd4n1HaJ/a4yuV960uRmPXaklJsedW0LNdcptd/hYUZtCkI7Lc9J5C1gxg==", + "requires": { + "@intlify/message-resolver": "9.1.10", + "@intlify/shared": "9.1.10", + "source-map": "0.6.1" + } + }, + "@intlify/message-resolver": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.10.tgz", + "integrity": "sha512-5YixMG/M05m0cn9+gOzd4EZQTFRUu8RGhzxJbR1DWN21x/Z3bJ8QpDYj6hC4FwBj5uKsRfKpJQ3Xqg98KWoA+w==" + }, + "@intlify/runtime": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.10.tgz", + "integrity": "sha512-7QsuByNzpe3Gfmhwq6hzgXcMPpxz8Zxb/XFI6s9lQdPLPe5Lgw4U1ovRPZTOs6Y2hwitR3j/HD8BJNGWpJnOFA==", + "requires": { + "@intlify/message-compiler": "9.1.10", + "@intlify/message-resolver": "9.1.10", + "@intlify/shared": "9.1.10" + } + }, + "@intlify/shared": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.10.tgz", + "integrity": "sha512-Om54xJeo1Vw+K1+wHYyXngE8cAbrxZHpWjYzMR9wCkqbhGtRV5VLhVc214Ze2YatPrWlS2WSMOWXR8JktX/IgA==" + }, + "@intlify/vue-devtools": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.10.tgz", + "integrity": "sha512-5l3qYARVbkWAkagLu1XbDUWRJSL8br1Dj60wgMaKB0+HswVsrR6LloYZTg7ozyvM621V6+zsmwzbQxbVQyrytQ==", + "requires": { + "@intlify/message-resolver": "9.1.10", + "@intlify/runtime": "9.1.10", + "@intlify/shared": "9.1.10" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@layui/icons-vue": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@layui/icons-vue/-/icons-vue-1.1.0.tgz", + "integrity": "sha512-ndc53qyUZSslUkO8ZHeBMh6i4gSTtAUqsPpKQZWML0JH6E/X3LIySe6LATeqEMmD7wWSnHJ+WBVGO4ij85Dk1g==" + }, + "@layui/layer-vue": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@layui/layer-vue/-/layer-vue-2.1.1.tgz", + "integrity": "sha512-lk9UoDQmLvtqrgdK+zeizp8KZy8pQfzX7dzHhAv+Qc74L1WC2jipb2hpYmaksiKX1lihy0D9eWWycMbnRn7V9A==", + "requires": { + "@layui/icons-vue": "1.1.0" + } + }, + "@layui/layui-vue": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@layui/layui-vue/-/layui-vue-2.11.5.tgz", + "integrity": "sha512-KZ5xrOm+B27yrEMWSuIGPLgLxUjISWuq0ecU4BcwrasCjEklfLS9UZBQp3peRWRsD6PGXP/cet1qQiD0AnUCJg==", + "requires": { + "@babel/types": "7.21.0", + "@ctrl/tinycolor": "^3.4.1", + "@layui/icons-vue": "1.1.0", + "@layui/layer-vue": "2.1.1", + "@rollup/plugin-terser": "0.4.3", + "@types/qrcode": "1.5.0", + "@umijs/ssr-darkreader": "^4.9.45", + "@vueuse/core": "8.7.3", + "async-validator": "^4.1.1", + "cropperjs": "^1.5.12", + "dayjs": "^1.11.7", + "evtd": "^0.2.3", + "jsbarcode": "3.11.5", + "qrcode": "1.5.0", + "vue-i18n": "9.1.10" + }, + "dependencies": { + "@vueuse/core": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.7.3.tgz", + "integrity": "sha512-jpBnyG9b4wXgk0Dz3I71lfhD0o53t1tZR+NoAQ+17zJy7MP/VDfGIkq8GcqpDwmptLCmGiGVipkPbWmDGMic8Q==", + "requires": { + "@vueuse/metadata": "8.7.3", + "@vueuse/shared": "8.7.3", + "vue-demi": "*" + } + }, + "@vueuse/metadata": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.7.3.tgz", + "integrity": "sha512-spf9kgCsBEFbQb90I6SIqAWh1yP5T1JoJGj+/04+VTMIHXKzn3iecmHUalg8QEOCPNtnFQGNEw5OLg0L39eizg==" + }, + "@vueuse/shared": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.7.3.tgz", + "integrity": "sha512-PMc/h6cEakJ4+5VuNUGi7RnbA6CkLvtG2230x8w3zYJpW1P6Qphh9+dFFvHn7TX+RlaicF5ND0RX1NxWmAoW7w==", + "requires": { + "vue-demi": "*" + } + }, + "vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "requires": {} + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@popperjs/core": { + "version": "npm:@sxzz/popperjs-es@2.11.7", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==" + }, + "@rollup/plugin-terser": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", + "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "requires": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + } + }, + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "@transloadit/prettier-bytes": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz", + "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==" + }, + "@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, + "@types/eslint": { + "version": "8.4.10", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.4.10.tgz", + "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "@types/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==" + }, + "@types/json-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz", + "integrity": "sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.201", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.201.tgz", + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==" + }, + "@types/lodash-es": { + "version": "4.17.11", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/node": { + "version": "18.11.17", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.11.17.tgz", + "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==" + }, + "@types/qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==", + "requires": { + "@types/node": "*" + } + }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, + "@types/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==", + "peer": true + }, + "@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz", + "integrity": "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.46.1", + "@typescript-eslint/type-utils": "5.46.1", + "@typescript-eslint/utils": "5.46.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-5.46.1.tgz", + "integrity": "sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.46.1", + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/typescript-estree": "5.46.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz", + "integrity": "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/visitor-keys": "5.46.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz", + "integrity": "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.46.1", + "@typescript-eslint/utils": "5.46.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-5.46.1.tgz", + "integrity": "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz", + "integrity": "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/visitor-keys": "5.46.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-5.46.1.tgz", + "integrity": "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.46.1", + "@typescript-eslint/types": "5.46.1", + "@typescript-eslint/typescript-estree": "5.46.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz", + "integrity": "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.46.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@umijs/ssr-darkreader": { + "version": "4.9.45", + "resolved": "https://registry.npmjs.org/@umijs/ssr-darkreader/-/ssr-darkreader-4.9.45.tgz", + "integrity": "sha512-XlcwzSYQ/SRZpHdwIyMDS4FOGX5kP4U/2g2mykyn/iPQTK4xTiQAyBu6UnnDnn7d5P8s7Atzh1C7H0ETNOypJg==" + }, + "@uppy/companion-client": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz", + "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==", + "requires": { + "@uppy/utils": "^4.1.2", + "namespace-emitter": "^2.0.1" + } + }, + "@uppy/core": { + "version": "2.3.4", + "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz", + "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==", + "requires": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/store-default": "^2.1.1", + "@uppy/utils": "^4.1.3", + "lodash.throttle": "^4.1.1", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^3.1.25", + "preact": "^10.5.13" + } + }, + "@uppy/store-default": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz", + "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==" + }, + "@uppy/utils": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==", + "requires": { + "lodash.throttle": "^4.1.1" + } + }, + "@uppy/xhr-upload": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz", + "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==", + "requires": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2", + "nanoid": "^3.1.25" + } + }, + "@vant/auto-import-resolver": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@vant/auto-import-resolver/-/auto-import-resolver-1.0.2.tgz", + "integrity": "sha512-5SYC1izl36KID+3F4pqFtYD8VFK6m1pdulft99sjSkUN4GBX9OslRnsJA0g7xS+0YrytjDuxxBk04YLYIxaYMg==", + "dev": true + }, + "@vant/popperjs": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz", + "integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==" + }, + "@vant/use": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.6.0.tgz", + "integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==", + "requires": {} + }, + "@vitejs/plugin-vue": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz", + "integrity": "sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==", + "dev": true, + "requires": {} + }, + "@volar/language-core": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.0.14.tgz", + "integrity": "sha512-j1tMQgw0qCV2amM4qDJNG/zc0yj3ay8HoWNt05IaiCPsULtSSpF/9+F6Izvn0DF7nWOd6MUHTxaQAeZwLfr56Q==", + "dev": true, + "requires": { + "@volar/source-map": "1.0.14", + "@vue/reactivity": "^3.2.45", + "muggle-string": "^0.1.0" + } + }, + "@volar/source-map": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.0.14.tgz", + "integrity": "sha512-8pHCbEWHWaSDGb/FM9zRIW1lY1OAo16MENVSQGCgTwz7PWf3Gw6WW3TFVKCtzaFhLjPH0i5e9hALy7vBPbSHoA==", + "dev": true, + "requires": { + "muggle-string": "^0.1.0" + } + }, + "@volar/typescript": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.0.14.tgz", + "integrity": "sha512-67qcjjz7KGFhMCG9EKMA9qJK3BRGQecO4dGyAKfMfClZ/PaVoKfDvJvYo89McGTQ8SeczD48I9TPnaJM0zK8JQ==", + "dev": true, + "requires": { + "@volar/language-core": "1.0.14" + } + }, + "@volar/vue-language-core": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/vue-language-core/-/vue-language-core-1.0.14.tgz", + "integrity": "sha512-grJ4dQ7c/suZmBBmZtw2O2XeDX+rtgpdBtHxMug1NMPRDxj5EZ9WGphWtGnMQj8RyVgpz9ByvV5GbQjk4/wfBw==", + "dev": true, + "requires": { + "@volar/language-core": "1.0.14", + "@volar/source-map": "1.0.14", + "@vue/compiler-dom": "^3.2.45", + "@vue/compiler-sfc": "^3.2.45", + "@vue/reactivity": "^3.2.45", + "@vue/shared": "^3.2.45", + "minimatch": "^5.1.0", + "vue-template-compiler": "^2.7.14" + } + }, + "@volar/vue-typescript": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@volar/vue-typescript/-/vue-typescript-1.0.14.tgz", + "integrity": "sha512-2P0QeGLLY05fDTu8GqY8SR2+jldXRTrkQdD2Nc0sVOjMJ7j3RYYY0wJyZ9hCBDuxV4Micc6jdB8nKS0yxQgNvA==", + "dev": true, + "requires": { + "@volar/typescript": "1.0.14", + "@volar/vue-language-core": "1.0.14" + } + }, + "@vue/compiler-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz", + "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==", + "requires": { + "@babel/parser": "^7.23.0", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz", + "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==", + "requires": { + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "@vue/compiler-sfc": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz", + "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==", + "requires": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-ssr": "3.3.8", + "@vue/reactivity-transform": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-ssr": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz", + "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==", + "requires": { + "@vue/compiler-dom": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "@vue/reactivity": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz", + "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==", + "requires": { + "@vue/shared": "3.3.8" + } + }, + "@vue/reactivity-transform": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz", + "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==", + "requires": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + } + }, + "@vue/runtime-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz", + "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==", + "requires": { + "@vue/reactivity": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "@vue/runtime-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz", + "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==", + "requires": { + "@vue/runtime-core": "3.3.8", + "@vue/shared": "3.3.8", + "csstype": "^3.1.2" + } + }, + "@vue/server-renderer": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz", + "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==", + "requires": { + "@vue/compiler-ssr": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "@vue/shared": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz", + "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==" + }, + "@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "requires": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "requires": {} + } + } + }, + "@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==" + }, + "@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "requires": { + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "requires": {} + } + } + }, + "@wangeditor/basic-modules": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz", + "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==", + "requires": { + "is-url": "^1.2.4" + } + }, + "@wangeditor/code-highlight": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz", + "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==", + "requires": { + "prismjs": "^1.23.0" + } + }, + "@wangeditor/core": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz", + "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==", + "requires": { + "@types/event-emitter": "^0.3.3", + "event-emitter": "^0.3.5", + "html-void-elements": "^2.0.0", + "i18next": "^20.4.0", + "scroll-into-view-if-needed": "^2.2.28", + "slate-history": "^0.66.0" + } + }, + "@wangeditor/editor": { + "version": "5.1.23", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz", + "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==", + "requires": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "^1.1.7", + "@wangeditor/code-highlight": "^1.0.3", + "@wangeditor/core": "^1.1.19", + "@wangeditor/list-module": "^1.0.5", + "@wangeditor/table-module": "^1.1.4", + "@wangeditor/upload-image-module": "^1.0.2", + "@wangeditor/video-module": "^1.1.4", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "@wangeditor/editor-for-vue": { + "version": "5.1.12", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz", + "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==", + "requires": {} + }, + "@wangeditor/list-module": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz", + "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==", + "requires": {} + }, + "@wangeditor/table-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz", + "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==", + "requires": {} + }, + "@wangeditor/upload-image-module": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz", + "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==", + "requires": {} + }, + "@wangeditor/video-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz", + "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==", + "requires": {} + }, + "ace-builds": { + "version": "1.32.2", + "resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.32.2.tgz", + "integrity": "sha512-mnJAc803p+7eeDt07r6XI7ufV7VdkpPq4gJZT8Jb3QsowkaBTVy4tdBgPrVT0WbXLm0toyEQXURKSVNj/7dfJQ==" + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axios": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "bpmn-js": { + "version": "7.5.0", + "resolved": "https://registry.npmmirror.com/bpmn-js/-/bpmn-js-7.5.0.tgz", + "integrity": "sha512-0ANaE6Bikg1GmkcvO7RK0MQPX+EKYKBc+q7OWk39/16NcCdNZ/4UiRcCr9n0u1VUCIDsSU/jJ79TIZFnV5CNjw==", + "dev": true, + "requires": { + "bpmn-moddle": "^7.0.4", + "css.escape": "^1.5.1", + "diagram-js": "^6.8.2", + "diagram-js-direct-editing": "^1.6.1", + "ids": "^1.0.0", + "inherits": "^2.0.4", + "min-dash": "^3.5.2", + "min-dom": "^3.1.3", + "object-refs": "^0.3.0", + "tiny-svg": "^2.2.2" + } + }, + "bpmn-js-properties-panel": { + "version": "0.37.6", + "resolved": "https://registry.npmmirror.com/bpmn-js-properties-panel/-/bpmn-js-properties-panel-0.37.6.tgz", + "integrity": "sha512-1rP9r6ItL1gKqXezXnpr9eVsQtdufH6TNqxUs11Q68CtxeBAs0l1wEHw2f01i9ceHHxItmrZUTndqnASi89EYA==", + "dev": true, + "requires": { + "@bpmn-io/extract-process-variables": "^0.3.0", + "ids": "^1.0.0", + "inherits": "^2.0.1", + "lodash": "^4.17.20", + "min-dom": "^3.1.3", + "scroll-tabs": "^1.0.1", + "selection-update": "^0.1.2" + } + }, + "bpmn-js-token-simulation": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.10.0.tgz", + "integrity": "sha512-QuZQ/KVXKt9Vl+XENyOBoTW2Aw+uKjuBlKdCJL6El7AyM7DkJ5bZkSYURshId1SkBDdYg2mJ1flSmsrhGuSfwg==", + "requires": { + "min-dash": "^3.3.0", + "min-dom": "^0.2.0", + "svg.js": "^2.6.3" + }, + "dependencies": { + "min-dom": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-0.2.0.tgz", + "integrity": "sha512-VmxugbnAcVZGqvepjhOA4d4apmrpX8mMaRS+/jo0dI5Yorzrr4Ru9zc9KVALlY/+XakVCb8iQ+PYXljihQcsNw==", + "requires": { + "component-classes": "^1.2.3", + "component-closest": "^0.1.4", + "component-delegate": "^0.2.3", + "component-event": "^0.1.4", + "component-matches-selector": "^0.1.5", + "component-query": "^0.0.3", + "domify": "^1.3.1" + } + } + } + }, + "bpmn-moddle": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-7.1.3.tgz", + "integrity": "sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==", + "dev": true, + "requires": { + "min-dash": "^3.5.2", + "moddle": "^5.0.2", + "moddle-xml": "^9.0.6" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "camunda-bpmn-moddle": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/camunda-bpmn-moddle/-/camunda-bpmn-moddle-4.5.0.tgz", + "integrity": "sha512-g3d2ZaCac52WIXP3kwmYrBEkhm0nnXcWYNj5STDkmiWpDTKUzTj4ZIt38IRpci1Uj3a/rZACvXLnQj8xKFyp/w==", + "dev": true, + "peer": true, + "requires": { + "min-dash": "^3.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001550", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001550.tgz", + "integrity": "sha512-p82WjBYIypO0ukTsd/FG3Xxs+4tFeaY9pfT4amQL8KWtYH7H9nYwReGAbMTJ0hsmRO8IfDtsS6p3ZWj8+1c2RQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "component-classes": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/component-classes/-/component-classes-1.2.6.tgz", + "integrity": "sha512-hPFGULxdwugu1QWW3SvVOCUHLzO34+a2J6Wqy0c5ASQkfi9/8nZcBB0ZohaEbXOQlCflMAEMmEWk7u7BVs4koA==", + "requires": { + "component-indexof": "0.0.3" + } + }, + "component-closest": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/component-closest/-/component-closest-0.1.4.tgz", + "integrity": "sha512-NF9hMj6JKGM5sb6wP/dg7GdJOttaIH9PcTsUNdWcrvu7Kw/5R5swQAFpgaYEHlARrNMyn4Wf7O1PlRej+pt76Q==", + "requires": { + "component-matches-selector": "~0.1.5" + } + }, + "component-delegate": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/component-delegate/-/component-delegate-0.2.4.tgz", + "integrity": "sha512-OlpcB/6Fi+kXQPh/TfXnSvvmrU04ghz7vcJh/jgLF0Ni+I+E3WGlKJQbBGDa5X+kVUG8WxOgjP+8iWbz902fPg==", + "requires": { + "component-closest": "*", + "component-event": "*" + } + }, + "component-event": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/component-event/-/component-event-0.1.4.tgz", + "integrity": "sha512-GMwOG8MnUHP1l8DZx1ztFO0SJTFnIzZnBDkXAj8RM2ntV2A6ALlDxgbMY1Fvxlg6WPQ+5IM/a6vg4PEYbjg/Rw==" + }, + "component-indexof": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/component-indexof/-/component-indexof-0.0.3.tgz", + "integrity": "sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==" + }, + "component-matches-selector": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/component-matches-selector/-/component-matches-selector-0.1.7.tgz", + "integrity": "sha512-Yb2+pVBvrqkQVpPaDBF0DYXRreBveXJNrpJs9FnFu8PF6/5IIcz5oDZqiH9nB5hbD2/TmFVN5ZCxBzqu7yFFYQ==", + "requires": { + "component-query": "*", + "global-object": "^1.0.0" + } + }, + "component-query": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/component-query/-/component-query-0.0.3.tgz", + "integrity": "sha512-VgebQseT1hz1Ps7vVp2uaSg+N/gsI5ts3AZUSnN6GMA2M82JH7o+qYifWhmVE/e8w/H48SJuA3nA9uX8zRe95Q==" + }, + "compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cropperjs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.1.tgz", + "integrity": "sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "requires": {} + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "cssdb": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/cssdb/-/cssdb-7.2.0.tgz", + "integrity": "sha512-JYlIsE7eKHSi0UNuCyo96YuIDFqvhGgHw4Ck6lsN+DP0Tp8M64UTDT2trGbkMDqnCoEjks7CkS0XcjU0rkvBdg==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "diagram-js": { + "version": "6.8.2", + "resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-6.8.2.tgz", + "integrity": "sha512-5EKYHjW2mmGsn9/jSenSkm8cScK5sO9eETBRQNIIzgZjxBDJn6eX964L2d7/vrAW9SeuijGUsztL9+NUinSsNg==", + "dev": true, + "requires": { + "css.escape": "^1.5.1", + "didi": "^4.0.0", + "hammerjs": "^2.0.1", + "inherits": "^2.0.1", + "min-dash": "^3.5.0", + "min-dom": "^3.1.2", + "object-refs": "^0.3.0", + "path-intersection": "^2.2.0", + "tiny-svg": "^2.2.1" + } + }, + "diagram-js-direct-editing": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-1.8.0.tgz", + "integrity": "sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==", + "dev": true, + "requires": { + "min-dash": "^3.5.2", + "min-dom": "^3.1.3" + } + }, + "didi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/didi/-/didi-4.0.0.tgz", + "integrity": "sha512-AzMElh8mCHOPWPCWfGjoJRla31fMXUT6+287W5ef3IPmtuBcyG9+MkFS7uPP6v3t2Cl086KwWfRB9mESa0OsHQ==", + "dev": true + }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "dom-zindex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dom-zindex/-/dom-zindex-1.0.1.tgz", + "integrity": "sha512-M/MERVDZ8hguvjl6MAlLWSLYLS7PzEyXaTb5gEeJ+SF+e9iUC0sdvlzqe91MMDHBoy+nqw7wKcUOrDSyvMCrRg==" + }, + "dom7": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz", + "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==", + "requires": { + "ssr-window": "^3.0.0-alpha.1" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domify": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/domify/-/domify-1.4.2.tgz", + "integrity": "sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==" + }, + "domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, + "echarts": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.5.0.tgz", + "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "requires": { + "tslib": "2.3.0", + "zrender": "5.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, + "ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.559", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.559.tgz", + "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==", + "dev": true + }, + "element-plus": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz", + "integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==", + "requires": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.3", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true + }, + "es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + } + }, + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "esbuild": { + "version": "0.16.9", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.16.9.tgz", + "integrity": "sha512-gkH83yHyijMSZcZFs1IWew342eMdFuWXmQo3zkDPTre25LIPBJsXryg02M3u8OpTwCJdBkdaQwqKkDLnAsAeLQ==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.16.9", + "@esbuild/android-arm64": "0.16.9", + "@esbuild/android-x64": "0.16.9", + "@esbuild/darwin-arm64": "0.16.9", + "@esbuild/darwin-x64": "0.16.9", + "@esbuild/freebsd-arm64": "0.16.9", + "@esbuild/freebsd-x64": "0.16.9", + "@esbuild/linux-arm": "0.16.9", + "@esbuild/linux-arm64": "0.16.9", + "@esbuild/linux-ia32": "0.16.9", + "@esbuild/linux-loong64": "0.16.9", + "@esbuild/linux-mips64el": "0.16.9", + "@esbuild/linux-ppc64": "0.16.9", + "@esbuild/linux-riscv64": "0.16.9", + "@esbuild/linux-s390x": "0.16.9", + "@esbuild/linux-x64": "0.16.9", + "@esbuild/netbsd-x64": "0.16.9", + "@esbuild/openbsd-x64": "0.16.9", + "@esbuild/sunos-x64": "0.16.9", + "@esbuild/win32-arm64": "0.16.9", + "@esbuild/win32-ia32": "0.16.9", + "@esbuild/win32-x64": "0.16.9" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.30.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.30.0.tgz", + "integrity": "sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.4.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "requires": {} + }, + "eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + } + }, + "eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "requires": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-vue": { + "version": "9.8.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.8.0.tgz", + "integrity": "sha512-E/AXwcTzunyzM83C2QqDHxepMzvI2y6x+mmeYHbVDQlKFqmKYvRrhaVixEeeG27uI44p9oKDFiyCRw4XxgtfHA==", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", + "postcss-selector-parser": "^6.0.9", + "semver": "^7.3.5", + "vue-eslint-parser": "^9.0.1", + "xml-name-validator": "^4.0.0" + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==" + }, + "ext": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "requires": { + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "global-object": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/global-object/-/global-object-1.0.0.tgz", + "integrity": "sha512-mSPSkY6UsHv6hgW0V2dfWBWTS8TnPnLx3ECVNoWp6rBI2Bg66VYoqGoTFlH/l7XhAZ/l+StYlntXlt87BEeCcg==" + }, + "globals": { + "version": "13.19.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "requires": { + "delegate": "^3.1.2" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "dev": true + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==" + }, + "html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" + }, + "htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, + "i18next": { + "version": "20.6.1", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz", + "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", + "requires": { + "@babel/runtime": "^7.12.0" + } + }, + "ids": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/ids/-/ids-1.0.5.tgz", + "integrity": "sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==", + "dev": true + }, + "ignore": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.2.tgz", + "integrity": "sha512-m1MJSy4Z2NAcyhoYpxQeBsc1ZdNQwYjN0wGbLBlnVArdJ90Gtr8IhNSfZZcCoR0fM/0E0BJ0mf1KnLNDOCJP4w==", + "dev": true + }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jake": { + "version": "10.8.7", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbarcode": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz", + "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==" + }, + "jsencrypt": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.3.2.tgz", + "integrity": "sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "requires": {} + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "matches-selector": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/matches-selector/-/matches-selector-1.2.0.tgz", + "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==", + "dev": true + }, + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "requires": { + "wildcard": "^1.1.0" + } + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "min-dash": { + "version": "3.8.1", + "resolved": "https://registry.npmmirror.com/min-dash/-/min-dash-3.8.1.tgz", + "integrity": "sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==" + }, + "min-dom": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-3.2.1.tgz", + "integrity": "sha512-v6YCmnDzxk4rRJntWTUiwggLupPw/8ZSRqUq0PDaBwVZEO/wYzCH4SKVBV+KkEvf3u0XaWHly5JEosPtqRATZA==", + "dev": true, + "requires": { + "component-event": "^0.1.4", + "domify": "^1.3.1", + "indexof": "0.0.1", + "matches-selector": "^1.2.0", + "min-dash": "^3.8.1" + } + }, + "minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true + }, + "mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "requires": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "moddle": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/moddle/-/moddle-5.0.4.tgz", + "integrity": "sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==", + "dev": true, + "requires": { + "min-dash": "^3.0.0" + } + }, + "moddle-xml": { + "version": "9.0.6", + "resolved": "https://registry.npmmirror.com/moddle-xml/-/moddle-xml-9.0.6.tgz", + "integrity": "sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==", + "dev": true, + "requires": { + "min-dash": "^3.5.2", + "moddle": "^5.0.2", + "saxen": "^8.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "muggle-string": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.1.0.tgz", + "integrity": "sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==", + "dev": true + }, + "namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==" + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-refs": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/object-refs/-/object-refs-0.3.0.tgz", + "integrity": "sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-intersection": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/path-intersection/-/path-intersection-2.2.1.tgz", + "integrity": "sha512-9u8xvMcSfuOiStv9bPdnRJQhGQXLKurew94n4GPQCdH1nj9QKC9ObbNoIpiRq8skiOBxKkt277PgOoFgAt3/rA==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pinia": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz", + "integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==", + "requires": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "requires": {} + } + } + }, + "pinia-plugin-persist": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/pinia-plugin-persist/-/pinia-plugin-persist-1.0.0.tgz", + "integrity": "sha512-M4hBBd8fz/GgNmUPaaUsC29y1M09lqbXrMAHcusVoU8xlQi1TqgkWnnhvMikZwr7Le/hVyMx8KUcumGGrR6GVw==", + "requires": { + "vue-demi": "^0.12.1" + }, + "dependencies": { + "vue-demi": { + "version": "0.12.5", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.12.5.tgz", + "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", + "requires": {} + } + } + }, + "pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmmirror.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmmirror.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmmirror.com/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "requires": {} + }, + "postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "dev": true, + "requires": {} + }, + "postcss-html": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/postcss-html/-/postcss-html-1.5.0.tgz", + "integrity": "sha512-kCMRWJRHKicpA166kc2lAVUGxDZL324bkj/pVOb6RhjB0Z5Krl7mN0AsVkBhVIRZZirY0lyQXG38HCVaoKVNoA==", + "dev": true, + "requires": { + "htmlparser2": "^8.0.0", + "js-tokens": "^8.0.0", + "postcss": "^8.4.0", + "postcss-safe-parser": "^6.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-8.0.0.tgz", + "integrity": "sha512-PC7MzqInq9OqKyTXfIvQNcjMkODJYC8A17kAaQgeW79yfhqTWSOfjHYQ2mDDcwJ96Iibtwkfh0C7R/OvqPlgVA==", + "dev": true + } + } + }, + "postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "requires": {} + }, + "postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "requires": {} + }, + "postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "requires": {} + }, + "postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "dev": true, + "requires": {} + }, + "postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "requires": {} + }, + "postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dev": true, + "requires": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmmirror.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "requires": {} + }, + "postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "requires": {} + }, + "postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "requires": {} + }, + "postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "preact": { + "version": "10.19.3", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.19.3.tgz", + "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-2.8.1.tgz", + "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==", + "requires": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "3.7.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.7.5.tgz", + "integrity": "sha512-z0ZbqHBtS/et2EEUKMrAl2CoSdwN7ZPzL17UMiKN9RjjqHShTlv7F9J6ZJZJNREYjBh3TvBrdfjkFDIXFNeuiQ==", + "devOptional": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "sass": { + "version": "1.57.1", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sax": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "saxen": { + "version": "8.1.2", + "resolved": "https://registry.npmmirror.com/saxen/-/saxen-8.1.2.tgz", + "integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==", + "dev": true + }, + "scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "requires": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "scroll-tabs": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/scroll-tabs/-/scroll-tabs-1.0.1.tgz", + "integrity": "sha512-W4xjEwNS4QAyQnaJ450vQTcKpbnalBAfsTDV926WrxEMOqjyj2To8uv2d0Cp0oxMdk5TkygtzXmctPNc2zgBcg==", + "dev": true, + "requires": { + "min-dash": "^3.1.0", + "min-dom": "^3.1.0", + "mitt": "^1.1.3" + } + }, + "scule": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.0.0.tgz", + "integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==", + "dev": true + }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" + }, + "selection-update": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/selection-update/-/selection-update-0.1.2.tgz", + "integrity": "sha512-4jzoJNh7VT2s2tvm/kUSskSw7pD0BVcrrGccbfOMK+3AXLBPz6nIy1yo+pbXgvNoTNII96Pq92+sAY+rF0LUAA==", + "dev": true + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slate": { + "version": "0.72.8", + "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz", + "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==", + "requires": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "slate-history": { + "version": "0.66.0", + "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz", + "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==", + "requires": { + "is-plain-object": "^5.0.0" + } + }, + "smob": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==" + }, + "snabbdom": { + "version": "3.5.1", + "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz", + "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "ssr-window": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz", + "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "requires": { + "acorn": "^8.10.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "tiny-svg": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/tiny-svg/-/tiny-svg-2.2.4.tgz", + "integrity": "sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==", + "dev": true + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmmirror.com/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "typescript": { + "version": "4.9.4", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "devOptional": true + }, + "ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unimport": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.4.0.tgz", + "integrity": "sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.4", + "escape-string-regexp": "^5.0.0", + "fast-glob": "^3.3.1", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.3", + "mlly": "^1.4.2", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "scule": "^1.0.0", + "strip-literal": "^1.3.0", + "unplugin": "^1.5.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true + } + } + }, + "unplugin": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.5.0.tgz", + "integrity": "sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==", + "dev": true, + "requires": { + "acorn": "^8.10.0", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "unplugin-auto-import": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.7.tgz", + "integrity": "sha512-w7XmnRlchq6YUFJVFGSvG1T/6j8GrdYN6Em9Wf0Ye+HXgD/22kont+WnuCAA0UaUoxtuvRR1u/mXKy63g/hfqQ==", + "dev": true, + "requires": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.5", + "fast-glob": "^3.3.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "minimatch": "^9.0.3", + "unimport": "^3.4.0", + "unplugin": "^1.5.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "requires": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "unplugin-vue-components": { + "version": "0.25.2", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.2.tgz", + "integrity": "sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==", + "dev": true, + "requires": { + "@antfu/utils": "^0.7.5", + "@rollup/pluginutils": "^5.0.2", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.0", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "minimatch": "^9.0.3", + "resolve": "^1.22.2", + "unplugin": "^1.4.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "vant": { + "version": "4.7.3", + "resolved": "https://registry.npmmirror.com/vant/-/vant-4.7.3.tgz", + "integrity": "sha512-nb0pXxKSOaE9CvH//KozKDivqhjE4ZRvx1b/RCWFL4H3tZ5l+HhWtwK1yJx5AkO1Pm/IYQY86yZa1tums8DfsQ==", + "requires": { + "@vant/popperjs": "^1.3.0", + "@vant/use": "^1.6.0", + "@vue/shared": "^3.0.0" + } + }, + "vite": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/vite/-/vite-4.0.2.tgz", + "integrity": "sha512-QJaY3R+tFlTagH0exVqbgkkw45B+/bXVBzF2ZD1KB5Z8RiAoiKo60vSUf6/r4c2Vh9jfGBKM4oBI9b4/1ZJYng==", + "dev": true, + "requires": { + "esbuild": "^0.16.3", + "fsevents": "~2.3.2", + "postcss": "^8.4.20", + "resolve": "^1.22.1", + "rollup": "^3.7.0" + } + }, + "vite-plugin-eslint": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", + "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^4.2.1", + "@types/eslint": "^8.4.5", + "rollup": "^2.77.2" + }, + "dependencies": { + "rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + } + } + }, + "vue": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz", + "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==", + "requires": { + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-sfc": "3.3.8", + "@vue/runtime-dom": "3.3.8", + "@vue/server-renderer": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "vue-draggable-plus": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.3.1.tgz", + "integrity": "sha512-Ubo0O8/D+hZPHb1bcDTjOE42a//OjLQwj+bQwfxa1WnEKTJdS7MU0A4auUcNjyIkhTN1xuETOR4mT+BGZCPL2g==", + "requires": {} + }, + "vue-eslint-parser": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz", + "integrity": "sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + } + }, + "vue-i18n": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.10.tgz", + "integrity": "sha512-jpr7gV5KPk4n+sSPdpZT8Qx3XzTcNDWffRlHV/cT2NUyEf+sEgTTmLvnBAibjOFJ0zsUyZlVTAWH5DDnYep+1g==", + "requires": { + "@intlify/core-base": "9.1.10", + "@intlify/shared": "9.1.10", + "@intlify/vue-devtools": "9.1.10", + "@vue/devtools-api": "^6.0.0-beta.7" + } + }, + "vue-json-viewer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/vue-json-viewer/-/vue-json-viewer-3.0.4.tgz", + "integrity": "sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==", + "requires": { + "clipboard": "^2.0.4" + } + }, + "vue-router": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.5.tgz", + "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==", + "requires": { + "@vue/devtools-api": "^6.5.0" + } + }, + "vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "vue-tsc": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.0.14.tgz", + "integrity": "sha512-HeqtyxMrSRUCnU5nxB0lQc3o7zirMppZ/V6HLL3l4FsObGepH3A3beNmNehpLQs0Gt7DkSWVi3CpVCFgrf+/sQ==", + "dev": true, + "requires": { + "@volar/vue-language-core": "1.0.14", + "@volar/vue-typescript": "1.0.14" + } + }, + "vxe-table": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/vxe-table/-/vxe-table-4.5.13.tgz", + "integrity": "sha512-CKsyUhDYIcO4TSXoO0I2YVkKEWjQLUq24PN6MhmFmvyFRdfj80cgLZ4iEjihLieW4aRqPcLHqkw83hCAyzvO8w==", + "requires": { + "dom-zindex": "^1.0.1", + "xe-utils": "^3.5.13" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "xe-utils": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/xe-utils/-/xe-utils-3.5.14.tgz", + "integrity": "sha512-Xq6mS8dWwHBQsQUEBXcZYSaBV0KnNLoVWd0vRRDI3nKpbNxfs/LSCK0W21g1edLFnXYfKqg7hh5dakr3RtYY0A==" + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zrender": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.5.0.tgz", + "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "requires": { + "tslib": "2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + } + } +} diff --git a/OrangeFormsOpen-VUE3/package.json b/OrangeFormsOpen-VUE3/package.json new file mode 100644 index 00000000..265e7294 --- /dev/null +++ b/OrangeFormsOpen-VUE3/package.json @@ -0,0 +1,71 @@ +{ + "name": "vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "lint": "eslint --fix \"src/**/*.{ts,vue}\" && prettier --write \"src/**/*.{ts,vue}\"" + }, + "dependencies": { + "@highlightjs/vue-plugin": "^2.1.0", + "@layui/layui-vue": "^2.11.5", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.12", + "ace-builds": "^1.32.2", + "axios": "^1.5.1", + "bpmn-js-token-simulation": "^0.10.0", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.10", + "echarts": "^5.5.0", + "ejs": "^3.1.9", + "highlight.js": "^11.9.0", + "jsencrypt": "^3.3.2", + "json-bigint": "^1.0.0", + "clipboard": "^2.0.11", + "pinia": "^2.1.6", + "pinia-plugin-persist": "^1.0.0", + "vant": "^4.7.3", + "vue": "^3.3.8", + "element-plus": "^2.7.3", + "vue-draggable-plus": "^0.3.1", + "vue-json-viewer": "^3.0.4", + "vue-router": "^4.2.5", + "vxe-table": "^4.5.13", + "xe-utils": "^3.5.14", + "xml-js": "^1.6.11" + }, + "devDependencies": { + "@types/ejs": "^3.1.5", + "@types/json-bigint": "^1.0.4", + "@types/node": "^18.11.17", + "@typescript-eslint/eslint-plugin": "^5.46.1", + "@typescript-eslint/parser": "^5.46.1", + "@vant/auto-import-resolver": "^1.0.2", + "@vitejs/plugin-vue": "^4.0.0", + "autoprefixer": "^10.4.16", + "bpmn-js": "^7.4.0", + "bpmn-js-properties-panel": "^0.37.2", + "eslint": "^8.30.0", + "eslint-config-prettier": "^8.5.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-vue": "^9.8.0", + "postcss": "^8.4.20", + "postcss-html": "^1.5.0", + "postcss-preset-env": "^7.8.3", + "postcss-scss": "^4.0.6", + "prettier": "2.8.1", + "sass": "^1.57.1", + "typescript": "^4.9.3", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "vite": "^4.0.0", + "vite-plugin-eslint": "^1.8.1", + "vue-eslint-parser": "^9.1.0", + "vue-tsc": "^1.0.11" + } +} diff --git a/OrangeFormsOpen-VUE3/public/favicon.ico b/OrangeFormsOpen-VUE3/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..eb9d6cdbaa8a48add912fb2bf51c393eeb074a20 GIT binary patch literal 4286 zcmchbZD`e17{E_o^lJ&er8Kb*O-w^jk)TjyQ6f^K`cNp-`G(rU?cUu@qYDa-`z`5&J3`l(MsXiSELOfoDqC!^r;0cKR**W1a^ zP>1h^yzk)y?6dcpGRg4#x|@>62HkheaR%R+LCJaV^&3O~e2*q?J?Yy=Tx;c|HLBky zVIoY1YIp*CHc&O}yj3BWyZ~+RH5`Yta1^$~TQHq|=E4{&l@sTBj=8tNCAbQma1i#w zG57_p!#T*&e*#o07q!fJ8g9TY=3NQPVLrSJS?r6U70gLD?8I&^`V)iep37Y4!S&^! z5xxO)u$$-aAzk-H_=0vlatr(cKA!~T?1f{dFo(JQ4ZRNLpF-P09$y|6Z8(Es~L{ojQxwB3h&$mPhx&;>8jPO}fL z{Tar62d9g3T}Hozz7na=+i;M!y1CZ(LS!cxn=u&gHQ0(? z_A1)%KnA@Q*@Rq#jK1xA4V$r^fK%vCL#cHzlD^jRb)>a@2>yY;it;S>8g!q1hWoSbyGkpTY+C7d&Gh;>)#KQ`Y7i=+=7NBV#rOeJ(?u zgOliK>Mx$ZGmQO_u}hKG@mBPnXY2rNYwQ7dF06Og+>P$K&7tRX6YVtT&vWYivljk@ zk7>UI)|_{7%yGPb+!Jla&;Wiu)X_c#U7;oQ{iFYQ|9;CH?rRG+^Y9Zy|E}NldN#Ew zKgaew(wcO?HbNCP=S+(+%wfE1?gsa}9zH_%uGxX!io5{F(LHxd!1})rUt;q+N~!lw z-|^)*yM_ICPIKy>FJazVa9#^pMyn7;I0;9wmES}_h84->%e7f(U_ z{y9*7V;-W^Pfwyfoy=CGKGOM&xz%^!y|cXWah`HzmUrGP@1a@V1 +import { useRoute } from 'vue-router'; +import zhCn from 'element-plus/dist/locale/zh-cn.mjs'; +import { useWindowResize } from '@/common/hooks/useWindowResize'; + +const route = useRoute(); + +watch( + () => route.name, + () => { + //console.log('路由发生了变化', route.name, route.fullPath, route.path, route); + document.title = import.meta.env.VITE_PROJECT_NAME; + if (route.meta && route.meta.title) { + document.title += ' - ' + route.meta.title; + } + }, +); + +useWindowResize(); + + + diff --git a/OrangeFormsOpen-VUE3/src/api/BaseController.ts b/OrangeFormsOpen-VUE3/src/api/BaseController.ts new file mode 100644 index 00000000..c84566a7 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/BaseController.ts @@ -0,0 +1,43 @@ +import { AxiosRequestConfig } from 'axios'; +import { commonRequest, download, downloadBlob, upload } from '@/common/http/request'; +import { RequestOption, RequestMethods } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; + +export class BaseController { + static async get( + url: string, + params: ANY_OBJECT, + options?: RequestOption, + axiosOption?: AxiosRequestConfig, + ) { + return await commonRequest(url, params, 'get', options, axiosOption); + } + static async post( + url: string, + params: ANY_OBJECT, + options?: RequestOption, + axiosOption?: AxiosRequestConfig, + ) { + return await commonRequest(url, params, 'post', options, axiosOption); + } + static download( + url: string, + params: ANY_OBJECT, + filename: string, + method?: RequestMethods, + options?: RequestOption, + ) { + return download(url, params, filename, method, options); + } + static downloadBlob( + url: string, + params: ANY_OBJECT, + method: RequestMethods = 'post', + options?: RequestOption, + ) { + return downloadBlob(url, params, method, options); + } + static upload(url: string, params: ANY_OBJECT, options?: RequestOption) { + return upload(url, params, options); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/config.ts b/OrangeFormsOpen-VUE3/src/api/config.ts new file mode 100644 index 00000000..9aa251a8 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/config.ts @@ -0,0 +1,2 @@ +// 服务前缀 admin or tenantadmin +export const API_CONTEXT = 'admin'; diff --git a/OrangeFormsOpen-VUE3/src/api/flow/FlowCategoryController.ts b/OrangeFormsOpen-VUE3/src/api/flow/FlowCategoryController.ts new file mode 100644 index 00000000..39556427 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/flow/FlowCategoryController.ts @@ -0,0 +1,31 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class FlowCategoryController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowCategory/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/flow/flowCategory/view', params, httpOptions); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowCategory/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowCategory/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowCategory/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/flow/FlowDictionaryController.ts b/OrangeFormsOpen-VUE3/src/api/flow/FlowDictionaryController.ts new file mode 100644 index 00000000..6e9afd67 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/flow/FlowDictionaryController.ts @@ -0,0 +1,23 @@ +import { DictData, DictionaryBase } from '@/common/staticDict/types'; +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class FlowDictionaryController extends BaseController { + static dictFlowCategory( + params: ANY_OBJECT, + httpOptions?: RequestOption, + ): Promise { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/flow/flowCategory/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/flow/FlowEntryController.ts b/OrangeFormsOpen-VUE3/src/api/flow/FlowEntryController.ts new file mode 100644 index 00000000..ba4b13f2 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/flow/FlowEntryController.ts @@ -0,0 +1,71 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class FlowEntryController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowEntry/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/flow/flowEntry/view', params, httpOptions); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/delete', params, httpOptions); + } + + static publish(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/publish', params, httpOptions); + } + + static listFlowEntryPublish(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/flow/flowEntry/listFlowEntryPublish', + params, + httpOptions, + ); + } + + static updateMainVersion(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/updateMainVersion', params, httpOptions); + } + + static suspendFlowEntryPublish(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/suspendFlowEntryPublish', params, httpOptions); + } + + static activateFlowEntryPublish(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntry/activateFlowEntryPublish', params, httpOptions); + } + + static viewDict(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/flow/flowEntry/viewDict', params, httpOptions); + } + + static listDict(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/flow/flowEntry/listDict', + params, + httpOptions, + ); + } + + static listAll(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get('/admin/flow/flowEntry/listAll', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/flow/FlowEntryVariableController.ts b/OrangeFormsOpen-VUE3/src/api/flow/FlowEntryVariableController.ts new file mode 100644 index 00000000..9214fd80 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/flow/FlowEntryVariableController.ts @@ -0,0 +1,31 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class FlowEntryVariableController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowEntryVariable/list', + params, + httpOptions, + ); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntryVariable/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntryVariable/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowEntryVariable/delete', params, httpOptions); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/flow/flowEntryVariable/view', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/flow/FlowOperationController.ts b/OrangeFormsOpen-VUE3/src/api/flow/FlowOperationController.ts new file mode 100644 index 00000000..9c512339 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/flow/FlowOperationController.ts @@ -0,0 +1,277 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class FlowOperationController extends BaseController { + // 保存草稿 + static startAndSaveDraft(params: ANY_OBJECT, httpOptions?: RequestOption) { + let url = API_CONTEXT + '/flow/flowOnlineOperation/startAndSaveDraft'; + if (httpOptions && httpOptions.processDefinitionKey) { + url += '/' + httpOptions.processDefinitionKey; + } + return this.post(url, params, httpOptions); + } + // 获取在线表单工作流草稿数据 + static viewOnlineDraftData(params: ANY_OBJECT, httpOptions?: RequestOption) { + const url = API_CONTEXT + '/flow/flowOnlineOperation/viewDraftData'; + return this.get(url, params, httpOptions); + } + // 启动流程实例并且提交表单信息 + static startAndTakeUserTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + let url = API_CONTEXT + '/flow/flowOnlineOperation/startAndTakeUserTask'; + if (httpOptions && httpOptions.processDefinitionKey) { + url += '/' + httpOptions.processDefinitionKey; + } else { + // 从流程设计里启动 + url = API_CONTEXT + '/flow/flowOnlineOperation/startPreview'; + } + return this.post(url, params, httpOptions); + } + // 获得流程以及工单信息 + static listWorkOrder(params: ANY_OBJECT, httpOptions?: RequestOption) { + let url = API_CONTEXT + '/flow/flowOnlineOperation/listWorkOrder'; + if (httpOptions && httpOptions.processDefinitionKey) { + url += '/' + httpOptions.processDefinitionKey; + } + return this.post>(url, params, httpOptions); + } + // 提交用户任务数据 + static submitUserTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOnlineOperation/submitUserTask', params, httpOptions); + } + // 获取历史流程数据 + static viewHistoricProcessInstance(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOnlineOperation/viewHistoricProcessInstance', + params, + httpOptions, + ); + } + // 获取用户任务数据 + static viewUserTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOnlineOperation/viewUserTask', + params, + httpOptions, + ); + } + // 获取在线表单工作流以及工作流下表单列表 + static listFlowEntryForm(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOnlineOperation/listFlowEntryForm', + params, + httpOptions, + ); + } + // 获得草稿信息 + static viewDraftData(params: ANY_OBJECT, httpOptions?: RequestOption) { + const url = API_CONTEXT + '/flow/flowOperation/viewDraftData'; + return this.get(url, params, httpOptions); + } + // 撤销工单 + static cancelWorkOrder(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/cancelWorkOrder', params, httpOptions); + } + // 多实例加签 + static submitConsign(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/submitConsign', params, httpOptions); + } + // 已办任务列表 + static listHistoricTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowOperation/listHistoricTask', + params, + httpOptions, + ); + } + // 获取已办任务信息 + static viewHistoricTaskInfo(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewHistoricTaskInfo', + params, + httpOptions, + ); + } + // 仅启动流程实例 + static startOnly(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/startOnly', params, httpOptions); + } + // 获得流程定义初始化用户任务信息 + static viewInitialTaskInfo(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewInitialTaskInfo', + params, + httpOptions, + ); + } + // 获取待办任务信息 + static viewRuntimeTaskInfo(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewRuntimeTaskInfo', + params, + httpOptions, + ); + } + // 获取流程实例审批历史 + static listFlowTaskComment(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/listFlowTaskComment', + params, + httpOptions, + ); + } + // 获取历史任务信息 + static viewInitialHistoricTaskInfo(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewInitialHistoricTaskInfo', + params, + httpOptions, + ); + } + // 获取所有待办任务 + static listRuntimeTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowOperation/listRuntimeTask', + params, + httpOptions, + ); + } + // 获得流程实例审批路径 + static viewHighlightFlowData(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewHighlightFlowData', + params, + httpOptions, + ); + } + // 获得流程实例的配置XML + static viewProcessBpmn(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewProcessBpmn', + params, + httpOptions, + ); + } + // 获得所有历史流程实例 + static listAllHistoricProcessInstance(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowOperation/listAllHistoricProcessInstance', + params, + httpOptions, + ); + } + // 获得当前用户历史流程实例 + static listHistoricProcessInstance(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowOperation/listHistoricProcessInstance', + params, + httpOptions, + ); + } + // 终止流程 + static stopProcessInstance(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/stopProcessInstance', params, httpOptions); + } + // 删除流程实例 + static deleteProcessInstance(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/flow/flowOperation/deleteProcessInstance', + params, + httpOptions, + ); + } + // 催办 + static remindRuntimeTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/remindRuntimeTask', params, httpOptions); + } + // 催办消息列表 + static listRemindingTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowMessage/listRemindingTask', + params, + httpOptions, + ); + } + // 驳回 + static rejectRuntimeTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/rejectRuntimeTask', params, httpOptions); + } + // 驳回到起点 + static rejectToStartUserTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/flow/flowOperation/rejectToStartUserTask', + params, + httpOptions, + ); + } + // 撤销 + static revokeHistoricTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/flow/flowOperation/revokeHistoricTask', params, httpOptions); + } + // 抄送消息列表 + static listCopyMessage(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/flow/flowMessage/listCopyMessage', + params, + httpOptions, + ); + } + // 消息个数 + static getMessageCount(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowMessage/getMessageCount', + params, + httpOptions, + ); + } + // 在线表单流程抄送消息数据 + static viewOnlineCopyBusinessData(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOnlineOperation/viewCopyBusinessData', + params, + httpOptions, + ); + } + // 静态表单流程抄送消息数据 + static viewCopyBusinessData(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewCopyBusinessData', + params, + httpOptions, + ); + } + // 获取指定任务处理人列表 + static viewTaskUserInfo(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/viewTaskUserInfo', + params, + httpOptions, + ); + } + // 获取驳回历史任务列表 + static listRejectCandidateUserTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/flow/flowOperation/listRejectCandidateUserTask', + params, + httpOptions, + ); + } + // 获取多实例任务中会签人员列表 + static listMultiSignAssignees(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/flow/flowOperation/listMultiSignAssignees', + params, + httpOptions, + ); + } + // 获取所有任务列表 + static listAllUserTask(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/flow/flowOperation/listAllUserTask', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/flow/index.ts b/OrangeFormsOpen-VUE3/src/api/flow/index.ts new file mode 100644 index 00000000..632dfd35 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/flow/index.ts @@ -0,0 +1,11 @@ +import FlowOperationController from './FlowOperationController'; +import FlowDictionaryController from './FlowDictionaryController'; +import FlowEntryController from './FlowEntryController'; +import FlowEntryVariableController from './FlowEntryVariableController'; + +export { + FlowOperationController, + FlowEntryController, + FlowDictionaryController, + FlowEntryVariableController, +}; diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineColumnController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineColumnController.ts new file mode 100644 index 00000000..6117e809 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineColumnController.ts @@ -0,0 +1,84 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { ColumnInfo } from '@/types/online/column'; +import { API_CONTEXT } from '../config'; + +export default class OnlineColumnController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineColumn/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineColumn/view', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlineColumn/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineColumn/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineColumn/update', params, httpOptions); + } + + static refreshColumn(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineColumn/refresh', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineColumn/delete', params, httpOptions); + } + + static listOnlineColumnRule(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineColumn/listOnlineColumnRule', + params, + httpOptions, + ); + } + + static listNotInOnlineColumnRule(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineColumn/listNotInOnlineColumnRule', + params, + httpOptions, + ); + } + + static addOnlineColumnRule(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineColumn/addOnlineColumnRule', params, httpOptions); + } + + static deleteOnlineColumnRule(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineColumn/deleteOnlineColumnRule', + params, + httpOptions, + ); + } + + static updateOnlineColumnRule(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineColumn/updateOnlineColumnRule', + params, + httpOptions, + ); + } + + static viewOnlineColumnRule(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineColumn/viewOnlineColumnRule', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceController.ts new file mode 100644 index 00000000..82b8b66c --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceController.ts @@ -0,0 +1,35 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class OnlineDatasourceController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineDatasource/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineDatasource/view', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlineDatasource/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDatasource/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDatasource/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDatasource/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceRelationController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceRelationController.ts new file mode 100644 index 00000000..02ab3aa7 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineDatasourceRelationController.ts @@ -0,0 +1,39 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class OnlineDatasourceRelationController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineDatasourceRelation/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlineDatasourceRelation/view', + params, + httpOptions, + ); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlineDatasourceRelation/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDatasourceRelation/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDatasourceRelation/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDatasourceRelation/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineDblinkController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineDblinkController.ts new file mode 100644 index 00000000..3178fb72 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineDblinkController.ts @@ -0,0 +1,53 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { DBLink } from '@/types/online/dblink'; +import { TableInfo } from '@/types/online/table'; +import { API_CONTEXT } from '../config'; + +export default class OnlineDblinkController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineDblink/list', + params, + httpOptions, + ); + } + + static listDblinkTables(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlineDblink/listDblinkTables', + params, + httpOptions, + ); + } + + static listDblinkTableColumns(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlineDblink/listDblinkTableColumns', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineDblink/view', params, httpOptions); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDblink/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDblink/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDblink/delete', params, httpOptions); + } + + static testConnection(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineDblink/testConnection', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineDictController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineDictController.ts new file mode 100644 index 00000000..997d48e5 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineDictController.ts @@ -0,0 +1,40 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { Dict } from '@/types/online/dict'; +import { API_CONTEXT } from '../config'; + +export default class OnlineDictController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>(API_CONTEXT + '/online/onlineDict/list', params, httpOptions); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineDict/view', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlineDict/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDict/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDict/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineDict/delete', params, httpOptions); + } + + static listAllGlobalDict(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineDict/listAllGlobalDict', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineFormController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineFormController.ts new file mode 100644 index 00000000..b4ac568d --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineFormController.ts @@ -0,0 +1,43 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class OnlineFormController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineForm/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineForm/view', params, httpOptions); + } + + static render(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineForm/render', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlineForm/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineForm/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineForm/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineForm/delete', params, httpOptions); + } + + static clone(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineForm/clone', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineOperationController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineOperationController.ts new file mode 100644 index 00000000..d8937a5f --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineOperationController.ts @@ -0,0 +1,91 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class OnlineOperationController extends BaseController { + static listDict(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineOperation/listDict', + params, + httpOptions, + ); + } + + static listByDatasourceId(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineOperation/listByDatasourceId', + params, + httpOptions, + ); + } + + static listByOneToManyRelationId(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineOperation/listByOneToManyRelationId', + params, + httpOptions, + ); + } + + static addDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineOperation/addDatasource', params, httpOptions); + } + + static addOneToManyRelation(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineOperation/addOneToManyRelation', + params, + httpOptions, + ); + } + + static updateDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineOperation/updateDatasource', params, httpOptions); + } + + static updateOneToManyRelation(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineOperation/updateOneToManyRelation', + params, + httpOptions, + ); + } + + static viewByDatasourceId(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlineOperation/viewByDatasourceId', + params, + httpOptions, + ); + } + + static viewByOneToManyRelationId(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlineOperation/viewByOneToManyRelationId', + params, + httpOptions, + ); + } + + static deleteDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineOperation/deleteDatasource', params, httpOptions); + } + + static deleteOneToManyRelation(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlineOperation/deleteOneToManyRelation', + params, + httpOptions, + ); + } + + static getColumnRuleCode(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineOperation/getColumnRuleCode', params, httpOptions); + } + + static getPrintTemplate(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/report/reportPrint/listAll', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlinePageController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlinePageController.ts new file mode 100644 index 00000000..623c1ff6 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlinePageController.ts @@ -0,0 +1,100 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { FormPage } from '@/types/online/page'; +import { API_CONTEXT } from '../config'; + +export default class OnlinePageController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlinePage/list', + params, + httpOptions, + ); + } + + static listAllPageAndForm(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlinePage/listAllPageAndForm', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlinePage/view', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlinePage/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlinePage/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlinePage/update', params, httpOptions); + } + + static updatePublished(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlinePage/updatePublished', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlinePage/delete', params, httpOptions); + } + + static updateStatus(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlinePage/updateStatus', params, httpOptions); + } + + static listOnlinePageDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlinePage/listOnlinePageDatasource', + params, + httpOptions, + ); + } + + static listNotInOnlinePageDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlinePage/listNotInOnlinePageDatasource', + params, + httpOptions, + ); + } + + static addOnlinePageDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlinePage/addOnlinePageDatasource', + params, + httpOptions, + ); + } + + static deleteOnlinePageDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlinePage/deleteOnlinePageDatasource', + params, + httpOptions, + ); + } + + static updateOnlinePageDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/online/onlinePage/updateOnlinePageDatasource', + params, + httpOptions, + ); + } + + static viewOnlinePageDatasource(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlinePage/viewOnlinePageDatasource', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineRuleController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineRuleController.ts new file mode 100644 index 00000000..bd753b2e --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineRuleController.ts @@ -0,0 +1,35 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { TableData } from '@/common/types/table'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class OnlineRuleController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineRule/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/online/onlineRule/view', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/online/onlineRule/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineRule/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineRule/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineRule/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/OnlineVirtualColumnController.ts b/OrangeFormsOpen-VUE3/src/api/online/OnlineVirtualColumnController.ts new file mode 100644 index 00000000..74ae18fd --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/OnlineVirtualColumnController.ts @@ -0,0 +1,35 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { TableData } from '@/common/types/table'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class OnlineVirtualColumnController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/online/onlineVirtualColumn/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/online/onlineVirtualColumn/view', + params, + httpOptions, + ); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineVirtualColumn/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineVirtualColumn/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/online/onlineVirtualColumn/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/online/index.ts b/OrangeFormsOpen-VUE3/src/api/online/index.ts new file mode 100644 index 00000000..1979f238 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/online/index.ts @@ -0,0 +1,23 @@ +import OnlineDblinkController from './OnlineDblinkController'; +import OnlineDictController from './OnlineDictController'; +import OnlinePageController from './OnlinePageController'; +import OnlineDatasourceRelationController from './OnlineDatasourceRelationController'; +import OnlineDatasourceController from './OnlineDatasourceController'; +import OnlineColumnController from './OnlineColumnController'; +import OnlineRuleController from './OnlineRuleController'; +import OnlineVirtualColumnController from './OnlineVirtualColumnController'; +import OnlineOperationController from './OnlineOperationController'; +import OnlineFormController from './OnlineFormController'; + +export { + OnlineDblinkController, + OnlineDictController, + OnlinePageController, + OnlineDatasourceRelationController, + OnlineDatasourceController, + OnlineColumnController, + OnlineRuleController, + OnlineVirtualColumnController, + OnlineOperationController, + OnlineFormController, +}; diff --git a/OrangeFormsOpen-VUE3/src/api/system/DictionaryController.ts b/OrangeFormsOpen-VUE3/src/api/system/DictionaryController.ts new file mode 100644 index 00000000..9477a3ce --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/DictionaryController.ts @@ -0,0 +1,217 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { DictData, DictionaryBase } from '@/common/staticDict/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class DictionaryController extends BaseController { + static dictSysRole(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/sysRole/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('角色字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + // 全局编码字典 + static dictGlobalDict(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/globalDict/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase( + '编码字典', + (res.data || []).map(item => { + return { + ...item, + // 设置已禁用编码字典数据项 + disabled: item.status === 1, + }; + }), + ); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictGlobalDictByIds(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/globalDict/listDictByIds', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('编码字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictSysDept(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/sysDept/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('部门字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictSysDeptByParentId( + params: ANY_OBJECT, + httpOptions?: RequestOption, + ): Promise { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/sysDept/listDictByParentId', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('部门字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictSysMenu(params: ANY_OBJECT, httpOptions?: RequestOption): Promise { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/sysMenu/listMenuDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('菜单字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption): Promise { + return new Promise((resolve, reject) => { + this.get( + API_CONTEXT + '/upms/sysDept/listSysDeptPostWithRelation', + params, + httpOptions, + ) + .then(res => { + resolve(res.data); + }) + .catch(err => { + reject(err); + }); + }); + } + static dictSysPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/upms/sysPost/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('岗位字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictReportDblink(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/report/reportDblink/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('数据库链接', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictReportDict(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/report/reportDict/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('报表字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + static dictAreaCode(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/app/areaCode/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('行政区划', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictAreaCodeByParentId(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/app/areaCode/listDictByParentId', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('行政区划', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + // 业务相关的接口 + static dictKnowledge(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get('/admin/app/knowledge/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('知识点字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictStudent(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get(API_CONTEXT + '/app/student/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('学生字典', res.data); + dictData.setList(res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } + + static dictTeacher(params: ANY_OBJECT, httpOptions?: RequestOption) { + return new Promise((resolve, reject) => { + this.get('/admin/app/teacher/listDict', params, httpOptions) + .then(res => { + const dictData = new DictionaryBase('老师字典', res.data); + resolve(dictData); + }) + .catch(err => { + reject(err); + }); + }); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/LoginController.ts b/OrangeFormsOpen-VUE3/src/api/system/LoginController.ts new file mode 100644 index 00000000..80c44ee3 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/LoginController.ts @@ -0,0 +1,14 @@ +import { loginParam, LoginUserInfo } from '@/types/upms/login'; +import { UserInfo } from '@/types/upms/user'; +import { BaseController } from '@/api/BaseController'; +import { API_CONTEXT } from '../config'; + +export default class LoginController extends BaseController { + static login(params: loginParam) { + return this.post(API_CONTEXT + '/upms/login/doLogin', params); + } + + static logout() { + return this.post(API_CONTEXT + '/upms/login/doLogout', {}); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/LoginUserController.ts b/OrangeFormsOpen-VUE3/src/api/system/LoginUserController.ts new file mode 100644 index 00000000..c6176108 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/LoginUserController.ts @@ -0,0 +1,21 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { OnlineUser } from '@/types/upms/user'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class LoginUserController extends BaseController { + // 在线用户查询 + static listSysLoginUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/loginUser/list', + params, + httpOptions, + ); + } + // 强退 + static deleteSysLoginUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/loginUser/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/MenuController.ts b/OrangeFormsOpen-VUE3/src/api/system/MenuController.ts new file mode 100644 index 00000000..cb9ddb2f --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/MenuController.ts @@ -0,0 +1,47 @@ +import { MenuItem } from '@/types/upms/menu'; +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class SystemMenuController extends BaseController { + static getMenuPermList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysMenu/list', params, httpOptions); + } + + static addMenu(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysMenu/add', params, httpOptions); + } + + static updateMenu(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysMenu/update', params, httpOptions); + } + + static deleteMenu(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysMenu/delete', params, httpOptions); + } + + static viewMenu(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysMenu/view', params, httpOptions); + } + + static listMenuPermCode(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysMenu/listMenuPerm', params, httpOptions); + } + + static listSysPermByMenuIdWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/upms/sysMenu/listSysPermWithDetail', + params, + httpOptions, + ); + } + + static listSysUserByMenuIdWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/upms/sysMenu/listSysUserWithDetail', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/MobileEntryController.ts b/OrangeFormsOpen-VUE3/src/api/system/MobileEntryController.ts new file mode 100644 index 00000000..5066bac2 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/MobileEntryController.ts @@ -0,0 +1,42 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class MobileEntryController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/mobile/mobileEntry/list', + params, + httpOptions, + ); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/mobile/mobileEntry/view', params, httpOptions); + } + + // static export(sender, params, fileName) { + // return sender.download(API_CONTEXT + '/mobile/mobileEntry/export', params, fileName); + // } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/mobile/mobileEntry/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/mobile/mobileEntry/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/mobile/mobileEntry/delete', params, httpOptions); + } + + static uploadImage(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/mobile/mobileEntry/uploadImage', params, httpOptions); + } + + static downloadImage(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/mobile/mobileEntry/downloadImage', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/OperationLogController.ts b/OrangeFormsOpen-VUE3/src/api/system/OperationLogController.ts new file mode 100644 index 00000000..d63f12ed --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/OperationLogController.ts @@ -0,0 +1,15 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { TableData } from '@/common/types/table'; +import { ANY_OBJECT } from '@/types/generic'; +import { API_CONTEXT } from '../config'; + +export default class OperationLogController extends BaseController { + static listSysOperationLog(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysOperationLog/list', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/PermCodeController.ts b/OrangeFormsOpen-VUE3/src/api/system/PermCodeController.ts new file mode 100644 index 00000000..50cbe25a --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/PermCodeController.ts @@ -0,0 +1,13 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { PermCode } from '@/types/upms/permcode'; +import { Role } from '@/types/upms/role'; +import { User } from '@/types/upms/user'; +import { API_CONTEXT } from '../config'; + +export default class PermCodeController extends BaseController { + static getPermCodeList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/login/getAllPermCodes', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/PermController.ts b/OrangeFormsOpen-VUE3/src/api/system/PermController.ts new file mode 100644 index 00000000..54afc0fc --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/PermController.ts @@ -0,0 +1,86 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { MenuItem } from '@/types/upms/menu'; +import { Perm, PermModule } from '@/types/upms/perm'; +import { Role } from '@/types/upms/role'; +import { User } from '@/types/upms/user'; +import { API_CONTEXT } from '../config'; + +export default class PermController extends BaseController { + static getPermList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>(API_CONTEXT + '/upms/sysPerm/list', params, httpOptions); + } + + static viewPerm(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysPerm/view', params, httpOptions); + } + + static addPerm(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPerm/add', params, httpOptions); + } + + static updatePerm(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPerm/update', params, httpOptions); + } + + static deletePerm(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPerm/delete', params, httpOptions); + } + + static getAllPermList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post( + API_CONTEXT + '/upms/sysPermModule/listAll', + params, + httpOptions, + ); + } + + static getPermGroupList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPermModule/list', params, httpOptions); + } + + static addPermGroup(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPermModule/add', params, httpOptions); + } + + static updatePermGroup(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPermModule/update', params, httpOptions); + } + + static deletePermGroup(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPermModule/delete', params, httpOptions); + } + + static listSysUserByPermIdWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/upms/sysPerm/listSysUserWithDetail', + params, + httpOptions, + ); + } + + static listSysRoleByPermIdWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/upms/sysPerm/listSysRoleWithDetail', + params, + httpOptions, + ); + } + + static listSysMenuByPermIdWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get( + API_CONTEXT + '/upms/sysPerm/listSysMenuWithDetail', + params, + httpOptions, + ); + } + + static listSysPermByRoleIdWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/upms/sysRole/listSysPermWithDetail', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/SysCommonBizController.ts b/OrangeFormsOpen-VUE3/src/api/system/SysCommonBizController.ts new file mode 100644 index 00000000..592ac936 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/SysCommonBizController.ts @@ -0,0 +1,18 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { TableData } from '@/common/types/table'; +import { API_CONTEXT } from '../config'; + +export default class SysCommonBizController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/commonext/bizwidget/list', + params, + httpOptions, + ); + } + static viewByIds(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/commonext/bizwidget/view', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/SysDataPermController.ts b/OrangeFormsOpen-VUE3/src/api/system/SysDataPermController.ts new file mode 100644 index 00000000..d0dc1d74 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/SysDataPermController.ts @@ -0,0 +1,80 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { PermData } from '@/types/upms/permdata'; +import { User } from '@/types/upms/user'; +import { API_CONTEXT } from '../config'; + +export default class SysDataPermController extends BaseController { + /** + * @param params {dataPermId, dataPermName, deptIdListString} + */ + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDataPerm/add', params, httpOptions); + } + + /** + * @param params {dataPermId, dataPermName, deptIdListString} + */ + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDataPerm/update', params, httpOptions); + } + + /** + * @param params {dataPermId} + */ + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDataPerm/delete', params, httpOptions); + } + + /** + * @param params {dataPermName} + */ + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysDataPerm/list', + params, + httpOptions, + ); + } + + /** + * @param params {dataPermId} + */ + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysDataPerm/view', params, httpOptions); + } + + /** + * @param params {dataPermId, searchString} + */ + static listDataPermUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysDataPerm/listDataPermUser', + params, + httpOptions, + ); + } + + /** + * @param params {dataPermId, userIdListString} + */ + static addDataPermUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDataPerm/addDataPermUser', params, httpOptions); + } + + /** + * @param params {dataPermId, userId} + */ + static deleteDataPermUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDataPerm/deleteDataPermUser', params, httpOptions); + } + + static listNotInDataPermUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysDataPerm/listNotInDataPermUser', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/SysDeptController.ts b/OrangeFormsOpen-VUE3/src/api/system/SysDeptController.ts new file mode 100644 index 00000000..5c303c40 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/SysDeptController.ts @@ -0,0 +1,63 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { SysDept, SysDeptPost } from '@/types/upms/department'; +import { API_CONTEXT } from '../config'; + +export default class SysDeptController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>(API_CONTEXT + '/upms/sysDept/list', params, httpOptions); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysDept/view', params, httpOptions); + } + + static export(params: ANY_OBJECT, fileName: string) { + return this.download(API_CONTEXT + '/upms/sysDept/export', params, fileName); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDept/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDept/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDept/delete', params, httpOptions); + } + + static listNotInSysDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysDept/listNotInSysDeptPost', + params, + httpOptions, + ); + } + + static listSysDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysDept/listSysDeptPost', + params, + httpOptions, + ); + } + + static addSysDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDept/addSysDeptPost', params, httpOptions); + } + + static updateSysDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDept/updateSysDeptPost', params, httpOptions); + } + + static deleteSysDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysDept/deleteSysDeptPost', params, httpOptions); + } + + static viewSysDeptPost(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysDept/viewSysDeptPost', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/SysGlobalDictController.ts b/OrangeFormsOpen-VUE3/src/api/system/SysGlobalDictController.ts new file mode 100644 index 00000000..0b5800e9 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/SysGlobalDictController.ts @@ -0,0 +1,53 @@ +import { post, get } from '@/common/http/request'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { DictCode, DictCodeItem } from '@/types/upms/dict'; +import { API_CONTEXT } from '../config'; + +type listAllItemType = { + cachedResultList: DictCodeItem[]; + fullResultList: DictCodeItem[]; +}; + +export default class SysGlobalDictController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post>(API_CONTEXT + '/upms/globalDict/list', params, httpOptions); + } + + static listAll(params: ANY_OBJECT, httpOptions?: RequestOption) { + console.log(this); + return get(API_CONTEXT + '/upms/globalDict/listAll', params, httpOptions); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/delete', params, httpOptions); + } + + static addItem(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/addItem', params, httpOptions); + } + + static updateItem(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/updateItem', params, httpOptions); + } + + static updateItemStatus(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/updateItemStatus', params, httpOptions); + } + + static deleteItem(params: ANY_OBJECT, httpOptions?: RequestOption) { + return post(API_CONTEXT + '/upms/globalDict/deleteItem', params, httpOptions); + } + + static reloadCachedData(params: ANY_OBJECT, httpOptions?: RequestOption) { + return get(API_CONTEXT + '/upms/globalDict/reloadCachedData', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/SysPostController.ts b/OrangeFormsOpen-VUE3/src/api/system/SysPostController.ts new file mode 100644 index 00000000..1ccdb02a --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/SysPostController.ts @@ -0,0 +1,27 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { Post } from '@/types/upms/post'; +import { API_CONTEXT } from '../config'; + +export default class SysPostController extends BaseController { + static list(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>(API_CONTEXT + '/upms/sysPost/list', params, httpOptions); + } + + static view(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysPost/view', params, httpOptions); + } + + static add(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPost/add', params, httpOptions); + } + + static update(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPost/update', params, httpOptions); + } + + static delete(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysPost/delete', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/SystemRoleController.ts b/OrangeFormsOpen-VUE3/src/api/system/SystemRoleController.ts new file mode 100644 index 00000000..1fe72ba3 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/SystemRoleController.ts @@ -0,0 +1,61 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { Role } from '@/types/upms/role'; +import { User } from '@/types/upms/user'; +import { API_CONTEXT } from '../config'; + +export default class SystemRoleController extends BaseController { + static getRoleList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>(API_CONTEXT + '/upms/sysRole/list', params, httpOptions); + } + + static getRole(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysRole/view', params, httpOptions); + } + + static deleteRole(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysRole/delete', params, httpOptions); + } + + static addRole(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysRole/add', params, httpOptions); + } + + static updateRole(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysRole/update', params, httpOptions); + } + + /** + * @param params {roleId, searchString} + */ + static listRoleUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysRole/listUserRole', + params, + httpOptions, + ); + } + + static listNotInUserRole(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>( + API_CONTEXT + '/upms/sysRole/listNotInUserRole', + params, + httpOptions, + ); + } + + /** + * @param params {roleId, userIdListString} + */ + static addRoleUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysRole/addUserRole', params, httpOptions); + } + + /** + * @param params {roleId, userId} + */ + static deleteRoleUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysRole/deleteUserRole', params, httpOptions); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/UserController.ts b/OrangeFormsOpen-VUE3/src/api/system/UserController.ts new file mode 100644 index 00000000..12f06ccd --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/UserController.ts @@ -0,0 +1,62 @@ +import { BaseController } from '@/api/BaseController'; +import { RequestOption, TableData } from '@/common/http/types'; +import { ANY_OBJECT } from '@/types/generic'; +import { MenuItem } from '@/types/upms/menu'; +import { Perm } from '@/types/upms/perm'; +import { PermCode } from '@/types/upms/permcode'; +import { User, UserInfo } from '@/types/upms/user'; +import { API_CONTEXT } from '../config'; + +export default class SystemUserController extends BaseController { + static getUserList(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post>(API_CONTEXT + '/upms/sysUser/list', params, httpOptions); + } + + static addUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysUser/add', params, httpOptions); + } + + static updateUser(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysUser/update', params, httpOptions); + } + + static deleteUser(params: User, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysUser/delete', params, httpOptions); + } + + static viewMenu(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysMenu/view', params, httpOptions); + } + + static getUser(params: User, httpOptions?: RequestOption) { + return this.get(API_CONTEXT + '/upms/sysUser/view', params, httpOptions); + } + + static resetUserPassword(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.post(API_CONTEXT + '/upms/sysUser/resetPassword', params, httpOptions); + } + + static listSysPermWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/upms/sysUser/listSysPermWithDetail', + params, + httpOptions, + ); + } + + static listSysPermCodeWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/upms/sysUser/listSysPermCodeWithDetail', + params, + httpOptions, + ); + } + + static listSysMenuWithDetail(params: ANY_OBJECT, httpOptions?: RequestOption) { + return this.get>( + API_CONTEXT + '/upms/sysUser/listSysMenuWithDetail', + params, + httpOptions, + ); + } +} diff --git a/OrangeFormsOpen-VUE3/src/api/system/index.ts b/OrangeFormsOpen-VUE3/src/api/system/index.ts new file mode 100644 index 00000000..586dcbc3 --- /dev/null +++ b/OrangeFormsOpen-VUE3/src/api/system/index.ts @@ -0,0 +1,31 @@ +import DictionaryController from './DictionaryController'; +import LoginUserController from './LoginUserController'; +import SystemMenuController from './MenuController'; +import PermController from './PermController'; +import PermCodeController from './PermCodeController'; +import SysDataPermController from './SysDataPermController'; +import SysDeptController from './SysDeptController'; +import SystemRoleController from './SystemRoleController'; +import SystemUserController from './UserController'; +import SysPostController from './SysPostController'; +import MobileEntryController from './MobileEntryController'; +import SysGlobalDictController from './SysGlobalDictController'; +import OperationLogController from './OperationLogController'; +import SysCommonBizController from './SysCommonBizController'; + +export { + SystemMenuController, + PermController, + PermCodeController, + SystemUserController, + DictionaryController, + SysDeptController, + SysDataPermController, + SystemRoleController, + LoginUserController, + SysPostController, + MobileEntryController, + SysGlobalDictController, + OperationLogController, + SysCommonBizController, +}; diff --git a/OrangeFormsOpen-VUE3/src/assets/img/add.png b/OrangeFormsOpen-VUE3/src/assets/img/add.png new file mode 100644 index 0000000000000000000000000000000000000000..42e1f361b0d0fe8e4fa928eb3f38ab9a37edd86a GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|mV3H5hFJI~ z|M~yl-k6!SL2y1}Z*T9J?fLhgMQlnr86H#i!1TbCD<)lyjR${yd3pI}iey4sn%MXE z_w9chr=L^lXJPj7@rja@kT`SkkJiBh2NVt*I8eas`{(gY#`TtEZ*D}2FmGu6hgdHJuw%l&kf lv{i*Bdwxt)OX6W**z&SR@Uii-W}uT9JYD@<);T3K0RX@6V{rfg literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/advance-add-active.png b/OrangeFormsOpen-VUE3/src/assets/img/advance-add-active.png new file mode 100644 index 0000000000000000000000000000000000000000..cd0338256080649be2a2eeb8973c3f2cc6cf12c2 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|x;n{3Zr9&N{@4-S9g4?Q?=;KYK4MH40}&TMRasCKx%O*w;Y ze&Uf2_%#=*yfx( zb7oId62rCtxy+3Xx;$xTXFUa3wtNG_;bm>xW_He)!^LneDoZfcrb`~^JO)o!KbLh* G2~7atAX~No literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/advance-del-active.png b/OrangeFormsOpen-VUE3/src/assets/img/advance-del-active.png new file mode 100644 index 0000000000000000000000000000000000000000..3a98209cdbdf4c3f1782f40c99516af53e31e06a GIT binary patch literal 279 zcmV+y0qFjTP)Px#(Md!>R5(x7lRXXrVGxF&@rl|cbPk}T(l~;GPNGpPTZ(c~Xe2rkaRZHt(gjp5 zpKK*}!>y}bN?nGm?!(2T4rL5|@u(!Z)n!Y-K*sEzbt^y}iWm$bLWZ+= z9KHke=*Op!A%lMdbg7ZSL~DcuGGhJJ3Tdt|M_D9>Kxh!qw4d>wz&4Pzr09arG@i)c dJ=3ROdIfP5Y7tHxy^;U`002ovPDHLkV1m76Zvp@S literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/advance-del.png b/OrangeFormsOpen-VUE3/src/assets/img/advance-del.png new file mode 100644 index 0000000000000000000000000000000000000000..8c8dc485204710c6f63df4b6af6d9064030f78ad GIT binary patch literal 308 zcmV-40n7f0P)Px#?ny*JR5(v#WS|f*QmCH+rvbBO&2s<$|Gx#u0!Bv06KQE_JF!`Y)qwu~egQ5n zuG@@^j59!*{{R1<@$K6;)xyHU59o%#4VW=wMhqh(<6@`=kgX_c|Ns9F7h_~(j7dvN zTMBj^va0FRr#mn+GuNl3r8#1A?yOm}_W%9+w=pv_^8id9rvcNaPuF2&WMuyP_b($W zE9>vHw6v=v88CC^%z9*3q^GAh4lx6!PoM4s3Ik}Y{LReF+(MEU$c_ajCZ_oe3=9i! zB=LlQ|NbT7j7De*%w}L!A@(A(P+$0000X6hFJJ7 zoovhJ=qTcL{^?WK^$QjU+-Ua=;NYI{K`DvT=)9DQn1hqMNEG)5mbHG`;T)gpW>hBK znpynryKh>H{q#R4Cwne%epL~2WP>kX3D>+2d`F#Mo}7{zk|gT#ph4UH(;I=Rq=@}0 z@vMOnPGVMzeEMup#W?kZCN!E02}NlCVY=_SkxMGzaQzV;p^d?l?ZYkz#uk|zxzJbg zk88Q`9abm#2t|{?H3d-y3%dNDOTJf8G`{X#d%v+vGhoRRCb{2s;xh~SR4*9%CQR~V oU^mPx#^hrcPR5(wilQC+;FcgM=J#5GglEIJ+9deIcA{~M|>Ak^Fd;@PK2N1$Tpa*E7 z-C8i`SK7F75Uq}XbE^{Z3L4vX@=f;?+wf6h}fk8n91Nc$Z zh=?+4?R!(wD`>#EF=k(jG7X3l#n+yYmk zRt4wWLme}3#_l?~vx4{jiHH`=ygGO8gD(=)f@c690G1)ROE4p%v>K@DK}0s8m-=@H zMNy2BBpHW*Y97b&DNWO3D}ncZNklJ01ORr-{CH7eAl%v({0kxJFGksM5g%r1Z~y=R M07*qoM6N<$g3g?N!TPx$6-h)vR5(wilCMicQ545NUuQ79Xk1({h#&?T7_2r$>{bO4aX##_u)PgUf{3uD zO@mqt8Vm#d0~QuvkYNyJ5S%;m@WPx#?@2^KR5(wilC4TaQ5Z$n0v|#Uo54pA#9%ZC3WCKTh!_kOgJ6&eGh*;PEH;B! zEP~B!Fqq5+gW$lHBQxS1=E~eVC+}}@_K!^YobsLuAjuxEr0!1&{skmC2R49B^|=us zNe?&y-hi)~;{tsEF4fPW=H~}V=7Brl2{^3*ECSobBCw{u7TsS9v%o!YrFQBpBsl`M zfK|0W8e)MgtJnW!kmM2QsJE?ZOmYVF)oxorY3yCSXbUJ7y6R11K#~Js7g!k=;tViX z6W{`{GpKQ0eW}zq*ux=kTmvWv!y$cChBWDH$LShpFbeqw*i=jrqk8SS00000NkvXX Hu0mjf>pXa0 literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/collapse.png b/OrangeFormsOpen-VUE3/src/assets/img/collapse.png new file mode 100644 index 0000000000000000000000000000000000000000..ce4aeaf98d9a3615a6eb37103036d5289ea94f36 GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|4tcsbhFJJ- zon*+>>>%L!zu^>DCR5`RuE;fWEEtwPN;jKXgocz(S-8>H`R1SP z=Wi&?cx^e)5^RJVnjZo75Jycf>gDld$)gv6D@J_dtT;`OjPq zQ(7-()pRJHJij&2l_{au@U&9qTK~vtm8KhCTixFF_OZS%Pv0Z%gklfDX4XC1fxcnz MboFyt=akR{0E>=ip8x;= literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/datasource-active.png b/OrangeFormsOpen-VUE3/src/assets/img/datasource-active.png new file mode 100644 index 0000000000000000000000000000000000000000..b8b1afd90bf2d9d0b5ba2dfd31dacc20f2d744d2 GIT binary patch literal 539 zcmV+$0_6RPP)b`UEND^M#?E07ZGK&(LMAaptn2y;N~aj(Q00!#@34Mt zjQP;d{Qv@tP_F{#s5|QNHMHFTl-KVV6u8f*+k`8d5z+z@juxbBG2Sp}@}Z@~hUc;= z-90m~-W(I}77mk>p{eYCU}4d7hSOpln1^%d44F4sd_5{ns(#H)$0eXi@Frc`yf`uw=p@^Y!m=i9F8yR+{ zs zQrO0w0evZx6|N~8cMG?Hv8Mmy(}&8yrLv@}hhnYYKv8+{@kncyrYPm-x0U9}Usyx^ do-G(^!7nHH>`;}mfQkSB002ovPDHLkV1kPX|2F^t literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/datasource.png b/OrangeFormsOpen-VUE3/src/assets/img/datasource.png new file mode 100644 index 0000000000000000000000000000000000000000..37f99e718c8d38b0837292a360ba6003e62ec3e9 GIT binary patch literal 640 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 literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/default.jpg b/OrangeFormsOpen-VUE3/src/assets/img/default.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aa0237bb9afb81a6080db11e085fc90632084a9a GIT binary patch literal 20200 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%0DhU#n?ioy`d|OqI_>#0 literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/demo-h5-qrcode.png b/OrangeFormsOpen-VUE3/src/assets/img/demo-h5-qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..b47443b746743d636da370b5a87f0c39b9d4f68c GIT binary patch literal 39426 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 literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/density.png b/OrangeFormsOpen-VUE3/src/assets/img/density.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c4ff5601da81e247ce9382606238d4ccdd7fa1 GIT binary patch literal 455 zcmV;&0XY7NP)Px$fk{L`R7gwRm9b01KorK`OFQKM(9P0t*MGsqQCeLbwYaz`h?5SkZmxoYpo@Qm z(4ouq93)f43>|b#5%LDPlLt*n^=e9*EX|n9eLwEI?|VV0_`!Ml>*wLWtKDdjp5efYkAjEEOX zDQq^Ijq!LqjG~B^2BZQ~%JUSEzMI&0=37rd{OI&;Rf=p|IBvagbC~h;sojh;snA5IDt5UY#<}Z z1oi)U7hg!zHZ2$Jdp7U&6=;(C_%C<4COPVOORD(tJRjouH7?%ImH*2c^KEdwCP~7( zx0M0{?iM3v%1~v~O#%7rd*}rO+ygq_{}P-r^9V@V-D?FvaqFkmYk|*n2QSUWCBy5+ znsns4+XrZ?x4?i*lggb7g3K+*f{H%CB?d4s*scJ$iVT=y{x`9~fEnBNe1HVH{3F13 z02278^?-HD18dNBv5tb6bO2X;8RHW9r!_EJj45OSRX_px0LZ`u5PoAaXHpMo8$gci zj@p1zO#UPWb}0b{sLELgo%Fe|mDC2La&s}n?P&u8ObY}LX;d(areFv`(>|&SHZ>m7 zzXuFasj82HWQjcAcRPitouc8GBU>JcfV&D$OX86r6E9VW!%d#{$NE|y)xRK;WN3bM198nF(rkcd?Q>#VnV-UGH# zEn1Kq3ga8!#>IAZ!c@ZV(V9~s70!aXaCpSLnu-EJheu4fOeM!Mhze>Sy%~?N3Nk0R zTYl!7qV2A-157(1M{}<;@vgpfja3~1XXwlUM#)&9R4yx9>mi5e<|H%*AeYYCb!)AQ dW016+^BVvc$~emA_S^sf002ovPDHLkV1jZl8E^mq literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/document.png b/OrangeFormsOpen-VUE3/src/assets/img/document.png new file mode 100644 index 0000000000000000000000000000000000000000..a804b6613bc324c4df282e210eac5ad4f643b2ac GIT binary patch literal 751 zcmVyi=p9IV$}&3^i4_t)$oZ25zm_&m?+{SNRDDd+*< zf$X=CPpB>TG&<9DeOVN{5YTeI93#+77Aij)0ZvWh&8NK4TtLh5PI%z`-++tPAb=01 zp`WDJ%mAvi-URUdZX&^F08R&?FA0oDyk&UXAiRy^KGI-6o$d_antBX39_tfHuyicj z_D*BSJQXv5w&N{0$e{{E0lYR%{i01k%kuiD!FjaC+1Str$@Ov+bT1I_)xQ8Fpi9#j z^>=`EwnQ?;VhYaZU%N*~!yhq-I{8HOd=nUtuMES;IDsf&_-DDHZ;05950K@!Gv2eu z6;)gEA=ppO6$P|ycM6Cf!vho1)}bp9bjF>()0g61=UIBLX+b9O<7M6+*c%<6y3_SZf{x=& z*k0x#1kt-x#Fq{)i{u2mk}2&fcNzNZm(4MPw8PNaJ5dG80Gf>N0Gp`RTCwm@n7sEzQ+!)2{N(!scDX7Ustq4mkGi6k9uh7{7hKX0 zTT?K%+;Z;kFjW*zUoAxyjPA@~Rm=tJK>Zbx&}D#mCA}zM89W3aE2On&y+X?Ng0`V~bk0}3e&`H2Ex4lgo3fIpw=dB`1KHMFoDPo7jiS5G z?TS6}ptSv_UasS9V!4m%%XGZL7I3sTR0}v%vc+dbNRy(yUfx6}?u3$b13mg&B!3Ul hZ`4F@QL$Q~{{U>Oi$_20P_F<0002ovPDHLkV1ms!SE>L2 literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/down.png b/OrangeFormsOpen-VUE3/src/assets/img/down.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1633eb2c3324121f3d85f2c0d0da2e299eb3ee GIT binary patch literal 477 zcmV<30V4j1P)mbyc_%{-0d{a3^cT3+$oxL>u);FH zF93hd+YSH`n4;PZ2E1ZG$_LPdVHoBp;D$qTKp75s?@iOB+&bk$2<0uhf^&IJV?Y|L zf|LPu;U)o8LBjkewZX&%-36-UH*>uy=LY*=Zn9fv)LiMpO=eI5xnDjy<|a3&f;54f z+#rMepzqkNmqWXiKM_u5P&Kjc?~`p3U4XHE9&pUiSo@zfxJBjJSoMzD>4bEp`z{uE zO}#mq*eC{ow|G3!TWJ4L9k(9QV{Vjg-+71A(-V(z`k%y}@i4&y+FJDnZ4G__cwMzP T`y)aE00000NkvXXu0mjfYADT{ literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/empty.png b/OrangeFormsOpen-VUE3/src/assets/img/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..2ccc001b69146ea69e5256143d76cb42cff0dbaf GIT binary patch literal 8205 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 V7MaG{^Z!2JqoU@ADtU0o{{gkyQ>6d^ literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/eye_close.png b/OrangeFormsOpen-VUE3/src/assets/img/eye_close.png new file mode 100644 index 0000000000000000000000000000000000000000..f44bbf2c8ed7febc6a20c8cf6ed102f48fe98e04 GIT binary patch literal 771 zcmV+e1N{7nP)X8D&ShkP(b5w=YX+5XHf5um@(P%sz zgpS#axw}U05KfIUjqF898X#xLC4}5IRXM7 z4*_e%0WkwVDLD-!K)t#z6fRIp?B_+NjW~kF4nWZ!igc-v&P!jkjc`!h*6Z~?2QuyX zF$l;3UKR?4GB@y%(}gZFpL-2sr`H2{0!>UhrFCPJlQ;%!=Q_@(Bk&2;yB{h6MrEqw5+_QCC6r~v$L4^|$iJW!H>DuhZBSbBQp0Y%~E z<>lt$;^H>`Hz$)x9nW?AZt;7qR{J#`kB9gj;%|uOhvVbpL23FayNr--w_AUCdD(*u zPZ{efkDxF%SXo)=wOTC#D|8hCK*L{OUr%6kn`{@-98aQGaCerMmoJI{$qnG<=H>*t zeUssMcaJ;7_rcQA((vHmfGjUy&eq%88-Z@Z*e32B9W;TEki)~nULHc60g(0Q=jS^p zYDfk6{|CnT>+9>GD}$8v^z_sOND}c$%ESNu+S=N#t1Kq~*Vor%tt(kqh9en+JdEjM zk^G(xB=$xN-k>)#FDn2T{~Z$Anv`=0_ie)Se1{&&JJaTLpyxD8P9}h>tE(>9@RR_u zj_0O|?KHuRd3@VOxs1vvN-A^V!8Q-xXuunR3@bbbM@L7kG+jmjae4e9OCu8$vFT;S zCg+VtBeJM_OzH2euCAWjWFXf9ndmIJD7W8C^QHipjED9fT$y;n#74@zsi9fM$iWCH z<`&nO=t`US4uHuBgh9(CQ4ww(C}D|JR}XXhaR@b}G4SRE!XnK}i2x=z!mpbJ;vE1z z=3R|v%)_Xk!uVaig0Pa|`<*c6t1TxuRtKTyL;^r((A9VPx$nMp)JR5(wi)1gblK@I?|RJ?Y)_deY5y+iotLalWH5p#ca znEAb0t>!TZf>|NNB{Q#CYaf4t5OD|*4-oOr7_*FSAP9m*A;bm%OdDff2O(PP0{|E& zqC5b+e*$i0GMQawo+6?dSLlamtyd6nsaC6H%jNQ$>uCdD$Qd)&iD;o0P)f~7DUYKl z%9cu{=T^N7@O^*S^Sm1XxF({VR+CanNhzQp&;?P;dkF zdVMb*k56=-KnU^tCm@kXWIBOnv-$FC004>r;2x0o+bir>DwS+1mD+Rzov92=!4;KK zBT~u}0GMRvCl_lJMW?Y??5Zb)i@sOr$-ltNw?uT9PN&C&5bYEijmGOh;HQt}>K`X) Vrl;@ItPTJG002ovPDHLkV1iVO&pQAB literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/import.png b/OrangeFormsOpen-VUE3/src/assets/img/import.png new file mode 100644 index 0000000000000000000000000000000000000000..dfe699679a98ba0b0729baea417b626852c09002 GIT binary patch literal 531 zcmV+u0_^>XP)I;AM(YxFy-(0v$jMc&)W8r)&4LEd#u1`V7an`13zGLRTVHmcA z0xZw-2gXEpr+BN}30K6~Da+@T{;je*6ZOgJwu(^T# z0rp$jz_NC&fPC4au7LpQ0Fvwo@&%A#mygQ-g8?ZR(&7oKfAGLTQ4~xCf^5??JvRA_ z0>~kEfP7U26YLu|+h~9sl9t!$0cm4$8>Gk{DH4eOqXPB-JG&r_edO^M0FMClHK3}J zTER*&C}GoYl)aORinA%`d0vJ@FOlfCO9dQ#nB)rQo)*c&2eD^CwCs|@lB*a7ru=BybdBeupwHZzVNWEaa?ozoT2vWZW0|DDR z?nwZf)WBy(ZxZQEa;*w}@(=Z6=gtufX3IepDA85}_EoO(`=1KR9NdR0SNT`B8M0@R zK35;3ur?k1FZwoqoe{#AE{`^&MD7>_vAz*2LWvCOB~hudoCCHVLjw*~T`M?L!5?nP V3r9KgBuM}O002ovPDHLkV1g^9;rsvq literal 0 HcmV?d00001 diff --git a/OrangeFormsOpen-VUE3/src/assets/img/login.png b/OrangeFormsOpen-VUE3/src/assets/img/login.png new file mode 100644 index 0000000000000000000000000000000000000000..87130950f1e18b6c5fc4938ab356693fc847daba GIT binary patch literal 728962 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 z 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Á_Jaf#O*Lgh3^=H*S`Lrt`)2ieuQ1ku9IWYsJJD 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%0DCDks3JEJGjr!X~}X3Hl6!4N&KDa5g@R2P@40 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�p0AVGf=jE9Gr=!U+Hmvl4Huh7U>=Ipe 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

Wml6w{ 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